diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 98e455b..ff9c481 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,11 +66,13 @@ jobs: - '3.12' - '3.13' - '3.14' + - '3.15' # Windows runners are scarce, so only run the lowest and highest Python versions when enabled. # exclude: [ # {platform: windows-latest, python-version: '3.11'}, # {platform: windows-latest, python-version: '3.12'}, # {platform: windows-latest, python-version: '3.13'}, + # {platform: windows-latest, python-version: '3.14'}, # ] runs-on: ${{ matrix.platform }} @@ -86,7 +88,7 @@ jobs: fetch-depth: 0 # nicer to have all git history when debugging/for tests # setup-uv doesn't publish floating major/minor tags, so use an exact full tag. - - uses: astral-sh/setup-uv@v10.0.1 + - uses: astral-sh/setup-uv@v10.3.0 with: python-version: ${{ matrix.python-version }} enable-cache: false # we don't have lock files during initial CI checkout, so can't use them as cache key @@ -96,8 +98,10 @@ jobs: limit-access-to-actor: true # restrict to the user who kicked off pipeline if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} - # explicit bash command is necessary for Windows CI runner, otherwise it thinks it's cmd... + # Invoke Bash explicitly because Windows runners default to PowerShell. - run: bash .ci/run + env: + RUFF_OUTPUT_FORMAT: github docs: @@ -107,7 +111,7 @@ jobs: with: persist-credentials: false fetch-depth: 0 - - uses: astral-sh/setup-uv@v10.0.1 + - uses: astral-sh/setup-uv@v10.3.0 with: python-version: '3.12' enable-cache: false @@ -150,7 +154,7 @@ jobs: fetch-depth: 0 # pull all commits to correctly infer vcs version # setup-uv doesn't publish floating major/minor tags, so use an exact full tag. - - uses: astral-sh/setup-uv@v10.0.1 + - uses: astral-sh/setup-uv@v10.3.0 with: python-version: '3.12' enable-cache: false # we don't have lock files during initial CI checkout, so can't use them as cache key diff --git a/.gitignore b/.gitignore index 4b9e120..11a0c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -74,7 +74,6 @@ parts/ sdist/ var/ wheels/ -pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -104,7 +103,6 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ -pytestdebug.log cover/ # Translations @@ -126,7 +124,6 @@ instance/ # Sphinx documentation docs/_build/ -doc/_build/ # PyBuilder .pybuilder/ @@ -135,10 +132,6 @@ target/ # Jupyter Notebook .ipynb_checkpoints -# Quarto -/.quarto/ -/README_files/ - # IPython profile_default/ ipython_config.py @@ -146,7 +139,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -.python-version +# .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -188,7 +181,6 @@ venv/ ENV/ env.bak/ venv.bak/ -pythonenv* # Spyder project settings .spyderproject @@ -251,10 +243,14 @@ tags # Persistent undo [._]*.un~ -# profiling data -.prof - # End of https://www.toptal.com/developers/gitignore/api/vim,emacs,python # for now, deliberately ignoring so CI always runs against latest versions /uv.lock + +# Sphinx +doc/_build/ + +# Quarto +/.quarto/ +/README_files/ diff --git a/pyproject.toml b/pyproject.toml index d4f9897..c456a66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,9 @@ local_scheme = "dirty-tag" [tool.mypy] +# default targets when mypy is run without explicit ones +packages = ["orgparse"] + pretty = true show_error_context = true show_error_end = true @@ -105,14 +108,12 @@ enable_error_code = [ "unused-awaitable", ] -# an example of suppressing -# [[tool.mypy.overrides]] -# module = "my.config.repos.pdfannots.pdfannots" -# ignore_errors = true - -[tool.ty.analysis] +[tool.ty.rules] # suppressing rules/changing error level goes here +# Catch checks like `if value` on `int | None`, where 0 and None are both falsy. +truthiness-test-of-none-union = "error" + [tool.ty.src] exclude = ["testdata/external"] @@ -129,7 +130,7 @@ lint.ignore = [ "CPY", # copyright headers "D", # annoying nags about docstrings "N", # pep naming - "TCH", # type checking rules, mostly just suggests moving imports under TYPE_CHECKING + "TC", # type checking rules, mostly just suggests moving imports under TYPE_CHECKING "S", # bandit (security checks) -- tends to be not very useful, lots of nitpicks "DTZ", # datetimes checks -- complaining about missing tz and mostly false positives "FIX", # complains about fixmes/todos -- annoying @@ -243,11 +244,13 @@ quote-style = "preserve" [tool.pytest.ini_options] -testpaths = ["src/orgparse"] - # discover files that don't follow test_ naming. Useful to keep tests along with the source code python_files = ["*.py"] +# default targets when pytest is run without explicit ones (package names because of --pyargs below) +# Also collect the README examples. +testpaths = ["orgparse", "README.qmd"] + # this is necessary for --pyargs to discover implicit namespace packages correctly consider_namespace_packages = true @@ -255,6 +258,11 @@ consider_namespace_packages = true strict = true addopts = [ + # resolve targets as importable packages, so tests are collected from the installed package + "--pyargs", + + "--doctest-glob=README.qmd", + # prevent pytest cache from being created... it craps into project dir and I never use it anyway "-p", "no:cacheprovider", @@ -275,7 +283,13 @@ min_version = "4" # Relies on the correct version of Python installed. # We rely on CI for the test matrix. -env_list = ["ruff", "format", "tests", "mypy", "ty"] +env_list = [ + "ruff", + "format", + "tests", + "mypy", + "ty", +] # https://github.com/tox-dev/tox/issues/20#issuecomment-247788333 # Hack to prevent .tox from crapping into the project directory. @@ -287,6 +301,9 @@ pass_env = [ "CI", "CI_*", + # Let CI use GitHub annotations for Ruff lint and formatting diagnostics. + "RUFF_OUTPUT_FORMAT", + # Respect user's cache dirs to prevent tox from crapping into project dir. "PYTHONPYCACHEPREFIX", "MYPY_CACHE_DIR", @@ -347,8 +364,6 @@ dependency_groups = ["testing"] commands = [ [ "{env_python}", "-m", "pytest", - "--pyargs", "{[project]name}", - "--doctest-glob=README.qmd", "README.qmd", { replace = "posargs", default = [], extend = true }, ], ] @@ -359,7 +374,6 @@ dependency_groups = ["typecheck"] commands = [ [ "{env_python}", "-m", "mypy", - "-p", "{[project]name}", # uncomment if you need coverage # "--txt-report" , ".coverage.mypy", # "--html-report", ".coverage.mypy", diff --git a/src/orgparse/date.py b/src/orgparse/date.py index 671cc63..6589ee8 100644 --- a/src/orgparse/date.py +++ b/src/orgparse/date.py @@ -1,10 +1,10 @@ from __future__ import annotations -import datetime +import datetime as dt import re from datetime import timedelta -DateIsh = datetime.date | datetime.datetime +DateIsh = dt.date | dt.datetime def total_seconds(td: timedelta) -> float: @@ -144,7 +144,7 @@ def date_time_format(date: DateIsh) -> str: """ default_format_date = "%Y-%m-%d %a" default_format_datetime = "%Y-%m-%d %a %H:%M" - is_datetime = isinstance(date, datetime.datetime) + is_datetime = isinstance(date, dt.datetime) return date.strftime(default_format_datetime if is_datetime else default_format_date) @@ -216,7 +216,7 @@ def __init__( :type warning: tuple or None :arg warning: Deadline warning interval. - >>> OrgDate(datetime.date(2012, 2, 10)) + >>> OrgDate(dt.date(2012, 2, 10)) OrgDate((2012, 2, 10)) >>> OrgDate((2012, 2, 10)) OrgDate((2012, 2, 10)) @@ -233,7 +233,7 @@ def __init__( OrgDate can be created using unix timestamp: - >>> OrgDate(datetime.datetime.fromtimestamp(0)) == OrgDate(0) + >>> OrgDate(dt.datetime.fromtimestamp(0)) == OrgDate(0) True """ @@ -248,9 +248,9 @@ def __init__( def _to_date(date) -> DateIsh: if isinstance(date, (tuple, list)): if len(date) == 3: - return datetime.date(*date) + return dt.date(*date) elif len(date) > 3: - return datetime.datetime(*date) + return dt.datetime(*date) else: raise ValueError( "Automatic conversion to the datetime object " @@ -258,15 +258,15 @@ def _to_date(date) -> DateIsh: f"Only {len(date)} elements are in the given tuple '{date}'." ) elif isinstance(date, (int, float)): - return datetime.datetime.fromtimestamp(date) + return dt.datetime.fromtimestamp(date) else: return date @staticmethod def _date_to_tuple(date: DateIsh) -> tuple[int, ...]: - if isinstance(date, datetime.datetime): + if isinstance(date, dt.datetime): return tuple(date.timetuple()[:6]) - elif isinstance(date, datetime.date): + elif isinstance(date, dt.date): return tuple(date.timetuple()[:3]) else: raise TypeError(f"can't happen: {date} {type(date)}") @@ -305,7 +305,7 @@ def __str__(self) -> str: (x, y, z) = self._warning start += f" {x}{y}{z}" ret = f"{fence[0]}{start}{fence[1]}" - if end: + if end is not None: ret += f"--{fence[0]}{end}{fence[1]}" return ret @@ -370,7 +370,7 @@ def has_time(self) -> bool: True """ - return isinstance(self._start, datetime.datetime) + return isinstance(self._start, dt.datetime) def has_overlap(self, other) -> bool: """ @@ -403,7 +403,7 @@ def has_overlap(self, other) -> bool: return False def _datetime_in_range(self, date): - if not isinstance(date, (datetime.datetime, datetime.date)): + if not isinstance(date, (dt.datetime, dt.date)): return False asdt = self._as_datetime if asdt(self.start) <= asdt(date) <= asdt(self.end): @@ -411,13 +411,13 @@ def _datetime_in_range(self, date): return False @staticmethod - def _as_datetime(date) -> datetime.datetime: + def _as_datetime(date) -> dt.datetime: """ Convert the given date into datetime (if it already is, return it unmodified """ - if not isinstance(date, datetime.datetime): - return datetime.datetime(*date.timetuple()[:3]) + if not isinstance(date, dt.datetime): + return dt.datetime(*date.timetuple()[:3]) return date @staticmethod @@ -659,20 +659,20 @@ def from_str(cls, string: str) -> OrgDateClock: # second part starting with "--", does not exist for open clock dates has_end = bool(match.group(6)) - ymdhm2_dt: datetime.datetime | None + ymdhm2_dt: dt.datetime | None len_min: int | None if has_end: ymdhm2 = [int(d) for d in match.groups()[6:11]] hm3 = [int(d) for d in match.groups()[11:]] - ymdhm2_dt = datetime.datetime(*ymdhm2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + ymdhm2_dt = dt.datetime(*ymdhm2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] len_min = hm3[0] * 60 + hm3[1] else: ymdhm2_dt = None len_min = None return cls( - datetime.datetime(*ymdhm1), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + dt.datetime(*ymdhm1), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ymdhm2_dt, len_min, ) diff --git a/src/orgparse/tests/data/00_simple.py b/src/orgparse/tests/data/00_simple.py index 91efd44..23ad86c 100644 --- a/src/orgparse/tests/data/00_simple.py +++ b/src/orgparse/tests/data/00_simple.py @@ -16,7 +16,7 @@ def nodedict(i, level, todo=None, shallow_tags=None, tags=None) -> dict[str, Any def tags(nums) -> set[str]: - return set(map('TAG{0}'.format, nums)) # ty: ignore[invalid-return-type] + return set(map('TAG{0}'.format, nums)) data = [ diff --git a/src/orgparse/tests/test_date.py b/src/orgparse/tests/test_date.py index 9764f97..49d68cf 100644 --- a/src/orgparse/tests/test_date.py +++ b/src/orgparse/tests/test_date.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt from orgparse.date import ( OrgDate, @@ -10,11 +10,11 @@ def test_date_as_string() -> None: - testdate = datetime.date(2021, 9, 3) - testdate2 = datetime.date(2021, 9, 5) - testdatetime = datetime.datetime(2021, 9, 3, 16, 19, 13) - testdatetime2 = datetime.datetime(2021, 9, 3, 17, 0, 1) - testdatetime_nextday = datetime.datetime(2021, 9, 4, 0, 2, 1) + testdate = dt.date(2021, 9, 3) + testdate2 = dt.date(2021, 9, 5) + testdatetime = dt.datetime(2021, 9, 3, 16, 19, 13) + testdatetime2 = dt.datetime(2021, 9, 3, 17, 0, 1) + testdatetime_nextday = dt.datetime(2021, 9, 4, 0, 2, 1) assert str(OrgDate(testdate)) == "<2021-09-03 Fri>" assert str(OrgDate(testdatetime)) == "<2021-09-03 Fri 16:19>" @@ -44,5 +44,5 @@ def test_date_as_datetime() -> None: testdate = (2021, 9, 3) testdatetime = (2021, 9, 3, 16, 19, 13) - assert OrgDate._as_datetime(datetime.date(*testdate)) == datetime.datetime(*testdate, 0, 0, 0) - assert OrgDate._as_datetime(datetime.datetime(*testdatetime)) == datetime.datetime(*testdatetime) + assert OrgDate._as_datetime(dt.date(*testdate)) == dt.datetime(*testdate, 0, 0, 0) + assert OrgDate._as_datetime(dt.datetime(*testdatetime)) == dt.datetime(*testdatetime)