Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
name: CI
on: [workflow_dispatch, pull_request, push]
permissions:
contents: read
name: Run tests on supported versions of Python

on:
push:
# branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:

jobs:
test:
tests:
runs-on: ubuntu-latest
steps: [uses: fastai/workflows/nbdev-ci@master]
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v5

- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies and run tests
run: |
uv sync --all-extras

- name: Run tests in uv virtual environment using nbdev
run: |
uv run nbdev_test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,5 @@ checklink/cookies.txt

# Quarto
.quarto

uv.lock
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ include CONTRIBUTING.md
include README.md
recursive-include nbstata *
recursive-exclude * __pycache__
global-include *.md
global-include *.json
global-include *.txt
# Keep pkg data wheel-friendly alongside include-package-data in pyproject.toml
33 changes: 19 additions & 14 deletions nbs/01_config.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@
"#| export\n",
"def _win_find_path(_dir=None):\n",
" if _dir is None:\n",
" dirs = [r'C:\\Program Files\\Stata19',\n",
" dirs = [r'C:\\Program Files\\StataNow19',\n",
" r'C:\\Program Files\\Stata19',\n",
" r'C:\\Program Files\\StataNow18',\n",
" r'C:\\Program Files\\Stata18',\n",
" r'C:\\Program Files\\Stata17']\n",
" else:\n",
Expand Down Expand Up @@ -155,23 +157,25 @@
"def _mac_find_path(_dir=None):\n",
" \"\"\"\n",
" Attempt to find Stata path on macOS when not on user's PATH.\n",
" Modified from stata_kernel's original to only location \"Applications/Stata\". \n",
" Modified from stata_kernel's original to \"/Applications/StataNow\" and \"/Applications/Stata\".\n",
"\n",
" Returns:\n",
" (str): Path to Stata. Empty string if not found.\n",
" \"\"\"\n",
" if _dir is None:\n",
" _dir = '/Applications/Stata'\n",
" path = Path(_dir)\n",
" if not os.path.exists(path):\n",
" return ''\n",
" dirs = [r'/Applications/StataNow',\n",
" r'/Applications/Stata']\n",
" else:\n",
" try:\n",
" # find the application with the suffix .app\n",
" # example path: /Applications/Stata/StataMP.app\n",
" return str(next(path.glob(\"Stata*.app\")))\n",
" except StopIteration:\n",
" return ''"
" dirs = [_dir] \n",
" for this_dir in dirs:\n",
" path = Path(this_dir)\n",
" if os.path.exists(path):\n",
" try:\n",
" # find the application with the suffix .app\n",
" # example path: /Applications/Stata/StataMP.app\n",
" return str(next(path.glob(\"Stata*.app\")))\n",
" except StopIteration:\n",
" return ''"
]
},
{
Expand Down Expand Up @@ -422,12 +426,13 @@
"source": [
"#| export\n",
"def set_pystata_path(stata_dir=None):\n",
" stata_dir = stata_dir.strip('\"\\'')\n",
" if stata_dir is None:\n",
" stata_dir, _ = find_dir_edition()\n",
" if not os.path.isdir(stata_dir):\n",
" raise OSError(f'Specified stata_dir, \"{stata_dir}\", is not a valid directory path')\n",
" raise OSError(f'Specified stata_dir, {stata_dir}, is not a valid directory path')\n",
" if not os.path.isdir(os.path.join(stata_dir, 'utilities')):\n",
" raise OSError(f'Specified stata_dir, \"{stata_dir}\", is not Stata\\'s installation path')\n",
" raise OSError(f'Specified stata_dir, {stata_dir}, is not Stata\\'s installation path')\n",
" sys.path.append(os.path.join(stata_dir, 'utilities'))"
]
},
Expand Down
4 changes: 2 additions & 2 deletions nbs/09_magics.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"from fastcore.basics import patch_to\n",
"import re\n",
"import urllib\n",
"from pkg_resources import resource_filename\n",
"from nbstata._resources import resource_path\n",
"from bs4 import BeautifulSoup as bs\n",
"import configparser"
]
Expand Down Expand Up @@ -161,7 +161,7 @@
" \n",
" abbrev_dict = _construct_abbrev_dict()\n",
" \n",
" csshelp_default = resource_filename(\n",
" csshelp_default = resource_path(\n",
" 'nbstata', 'css/_StataKernelHelpDefault.css'\n",
" )\n",
"\n",
Expand Down
2 changes: 1 addition & 1 deletion nbs/15_install.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@
"outputs": [],
"source": [
"#| export\n",
"#|eval: false\n",
"#| eval: false\n",
"if __name__ == \"__main__\" and not IN_NOTEBOOK:\n",
" main()"
]
Expand Down
2 changes: 1 addition & 1 deletion nbstata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.8.3"
__version__ = "0.8.3.dev0"

from .config import launch_stata, set_graph_format
from . import misc_utils, config, stata, stata_more, code_utils, noecho, pandas
34 changes: 34 additions & 0 deletions nbstata/_resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Zip-safe, modern resource helpers for Python 3.10+ using importlib.resources.
Use these in place of pkg_resources.resource_filename / resource_string.
"""
from __future__ import annotations
from contextlib import contextmanager
from importlib.resources import files, as_file
from pathlib import Path
from typing import Iterator

PACKAGE = "nbstata"

def read_text(relpath: str, package: str = PACKAGE) -> str:
"""Read a packaged text resource."""
return (files(package) / relpath).read_text()

def read_bytes(relpath: str, package: str = PACKAGE) -> bytes:
"""Read a packaged binary resource."""
return (files(package) / relpath).read_bytes()

@contextmanager
def resource_path(relpath: str, package: str = PACKAGE) -> Iterator[Path]:
"""
Context manager yielding a temporary real filesystem Path for a packaged
resource. This mirrors pkg_resources.resource_filename semantics but is
zip-safe and deprecation-proof.
"""
with as_file(files(package) / relpath) as p:
yield p

def resource_strpath(relpath: str, package: str = PACKAGE) -> str:
"""Return a string path for cases that strictly want str over Path."""
with resource_path(relpath, package) as p:
return str(p)
16 changes: 16 additions & 0 deletions nbstata/_version_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Version helpers that avoid distutils and runtime pkg_resources.
"""
from __future__ import annotations
from packaging.version import Version, InvalidVersion

def version_at_least(v: str, minimum: str) -> bool:
"""
Return True if version string v >= minimum using robust PEP 440 parsing.
Replaces pkg_resources.parse_version / distutils.LooseVersion.
"""
try:
return Version(v) >= Version(minimum)
except InvalidVersion:
# Conservative fallback: treat unknown as not meeting the minimum
return False
33 changes: 19 additions & 14 deletions nbstata/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
# %% ../nbs/01_config.ipynb 8
def _win_find_path(_dir=None):
if _dir is None:
dirs = [r'C:\Program Files\Stata19',
dirs = [r'C:\Program Files\StataNow19',
r'C:\Program Files\Stata19',
r'C:\Program Files\StataNow18',
r'C:\Program Files\Stata18',
r'C:\Program Files\Stata17']
else:
Expand All @@ -45,23 +47,25 @@ def _win_find_path(_dir=None):
def _mac_find_path(_dir=None):
"""
Attempt to find Stata path on macOS when not on user's PATH.
Modified from stata_kernel's original to only location "Applications/Stata".
Modified from stata_kernel's original to "/Applications/StataNow" and "/Applications/Stata".

Returns:
(str): Path to Stata. Empty string if not found.
"""
if _dir is None:
_dir = '/Applications/Stata'
path = Path(_dir)
if not os.path.exists(path):
return ''
dirs = [r'/Applications/StataNow',
r'/Applications/Stata']
else:
try:
# find the application with the suffix .app
# example path: /Applications/Stata/StataMP.app
return str(next(path.glob("Stata*.app")))
except StopIteration:
return ''
dirs = [_dir]
for this_dir in dirs:
path = Path(this_dir)
if os.path.exists(path):
try:
# find the application with the suffix .app
# example path: /Applications/Stata/StataMP.app
return str(next(path.glob("Stata*.app")))
except StopIteration:
return ''

# %% ../nbs/01_config.ipynb 12
def _other_find_path():
Expand Down Expand Up @@ -110,12 +114,13 @@ def find_edition(stata_dir):

# %% ../nbs/01_config.ipynb 25
def set_pystata_path(stata_dir=None):
stata_dir = stata_dir.strip('"\'')
if stata_dir is None:
stata_dir, _ = find_dir_edition()
if not os.path.isdir(stata_dir):
raise OSError(f'Specified stata_dir, "{stata_dir}", is not a valid directory path')
raise OSError(f'Specified stata_dir, {stata_dir}, is not a valid directory path')
if not os.path.isdir(os.path.join(stata_dir, 'utilities')):
raise OSError(f'Specified stata_dir, "{stata_dir}", is not Stata\'s installation path')
raise OSError(f'Specified stata_dir, {stata_dir}, is not Stata\'s installation path')
sys.path.append(os.path.join(stata_dir, 'utilities'))

# %% ../nbs/01_config.ipynb 29
Expand Down
2 changes: 1 addition & 1 deletion nbstata/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,6 @@ def main(argv=None):
create_conf_if_needed(conf_path, conf_file_requested=args.conf_file)

# %% ../nbs/15_install.ipynb 15
#|eval: false
#| eval: false
if __name__ == "__main__" and not IN_NOTEBOOK:
main()
4 changes: 2 additions & 2 deletions nbstata/magics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from fastcore.basics import patch_to
import re
import urllib
from pkg_resources import resource_filename
from ._resources import resource_path
from bs4 import BeautifulSoup as bs
import configparser

Expand Down Expand Up @@ -71,7 +71,7 @@ class StataMagics():

abbrev_dict = _construct_abbrev_dict()

csshelp_default = resource_filename(
csshelp_default = resource_path(
'nbstata', 'css/_StataKernelHelpDefault.css'
)

Expand Down
55 changes: 55 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name="nbstata"
version = "0.8.3.dev0"
description = "A Jupyter kernel for Stata built on pystata"
readme = "README.md"
requires-python=">=3.10"
license = { text = "GPL-3.0-only" }
authors = [{ name = "Tim Huegerich" }]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)"
]
# Keep runtime deps minimal; pystata is provided by Stata itself.
dependencies = [
"bs4>=0.0.2",
"fastcore>=1.8.13",
"ipykernel>=6",
"jupyter-client>=7",
"nbclient>=0.10.2",
"nbformat>=5.10.4",
"numpy>=2.2.6",
"packaging>=23", # for Version parsing (instead of distutils/pkg_resources)
"pyyaml>=6.0.2",
]

[project.scripts]
# Mirrors the documented CLI usage: python -m nbstata.install
nbstata-install = "nbstata.install:main"

[tool.setuptools]
include-package-data = true
packages = { find = { where = ["."], include = ["nbstata*"] } }

# If you ship any data files within the package tree, ensure they're included.
# You already have MANIFEST.in; include-package-data keeps wheels aligned.

[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-q"
testpaths = ["nbs"]

[dependency-groups]
dev = [
"nbdev>=2.4.6",
]
7 changes: 3 additions & 4 deletions settings.ini
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[DEFAULT]
repo = nbstata
lib_name = nbstata
version = 0.8.3
min_python = 3.9
version = 0.8.3.dev0
min_python = 3.10
license = gpl3
doc_path = _docs
lib_path = nbstata
Expand Down Expand Up @@ -34,5 +34,4 @@ jupyter_hooks = True
clean_ids = True
clear_all = False
cell_number = True
skip_procs =

skip_procs =
Loading