Skip to content
Merged
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
12 changes: 8 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 8 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
Expand Down Expand Up @@ -104,7 +103,6 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
cover/

# Translations
Expand All @@ -126,7 +124,6 @@ instance/

# Sphinx documentation
docs/_build/
doc/_build/

# PyBuilder
.pybuilder/
Expand All @@ -135,18 +132,14 @@ target/
# Jupyter Notebook
.ipynb_checkpoints

# Quarto
/.quarto/
/README_files/

# IPython
profile_default/
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.
Expand Down Expand Up @@ -188,7 +181,6 @@ venv/
ENV/
env.bak/
venv.bak/
pythonenv*

# Spyder project settings
.spyderproject
Expand Down Expand Up @@ -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/
40 changes: 27 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
Expand All @@ -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
Expand Down Expand Up @@ -243,18 +244,25 @@ 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

# see https://docs.pytest.org/en/stable/reference/reference.html#confval-strict
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",

Expand All @@ -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.
Expand All @@ -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",
Expand Down Expand Up @@ -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 },
],
]
Expand All @@ -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",
Expand Down
38 changes: 19 additions & 19 deletions src/orgparse/date.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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))
Expand All @@ -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

"""
Expand All @@ -248,25 +248,25 @@ 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 "
"requires at least 3 elements in the tuple. "
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)}")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -403,21 +403,21 @@ 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):
return True
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
Expand Down Expand Up @@ -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,
)
Expand Down
2 changes: 1 addition & 1 deletion src/orgparse/tests/data/00_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Loading
Loading