diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index e7c91cd..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,16 +0,0 @@ -[bumpversion] -current_version = 1.1.1 -commit = True -tag = True -message = Release {new_version} - -[bumpversion:file:sedate/__init__.py] - -[bumpversion:file:HISTORY.rst] -search = - --------- -replace = - --------- - - {new_version} ({now:%Y-%m-%d}) - ~~~~~~~~~~~~~~~~~~~ diff --git a/.github/workflows/python-pr.yaml b/.github/workflows/python-pr.yaml index 1d8c647..726e824 100644 --- a/.github/workflows/python-pr.yaml +++ b/.github/workflows/python-pr.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] + python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -21,7 +21,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install tox tox-gh-actions tox-uv - name: Test with tox without uploading coverage run: tox diff --git a/.github/workflows/python-tox.yaml b/.github/workflows/python-tox.yaml index c966fa2..5ebcb47 100644 --- a/.github/workflows/python-tox.yaml +++ b/.github/workflows/python-tox.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] + python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install tox tox-gh-actions tox-uv - name: Test with tox and upload coverage results run: tox -- --codecov --codecov-token=${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa38af7..0308ae9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace exclude: '^(.bumpversion.cfg)$' @@ -13,20 +13,25 @@ repos: hooks: - id: rst-linter files: '^[A-Z]+\.rst$' - additional_dependencies: - - types_requests - repo: https://github.com/seantis/pre-commit-hooks rev: v1.1.0 hooks: - id: nocheckin exclude: .pre-commit-config.yaml + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.1 + hooks: + - id: ruff + args: [ "--fix" ] - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: - id: flake8 + additional_dependencies: + - flake8-type-checking files: '^(sedate/.*|tests/.*)\.py$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.14.1 hooks: - id: mypy files: '^sedate/.*\.py$' diff --git a/HISTORY.rst b/HISTORY.rst index eaa8d63..d39f59d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,9 @@ Changelog --------- +- Adds support for Python 3.13. Drops support for Python 3.8 + [Daverball] + 1.1.1 (2024-08-13) ~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index a69674b..f5b138a 100644 --- a/README.rst +++ b/README.rst @@ -37,12 +37,12 @@ Run the Tests Install tox and run it:: - pip install tox + pip install tox tox-uv tox Limit the tests to a specific python version:: - tox -e py37 + tox -e py311 Conventions ----------- @@ -59,9 +59,9 @@ Development Setup your local development environment:: - python3.8 -m venv venv + python3 -m venv venv source venv/bin/activate - pip install .[dev] + pip install -e .[dev] pre-commit install License diff --git a/pyproject.toml b/pyproject.toml index 1a8c35f..69391df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,32 @@ exclude_lines = [ "raise NotImplementedError" ] +[tool.bumpversion] +current_version = "1.1.1" +commit = true +message = "Release {new_version}" +tag = true +tag_message = "Release {new_version}" + +[[tool.bumpversion.files]] +filename = "sedate/__init__.py" +search= "__version__ = '{current_version}'" +replace= "__version__ = '{new_version}'" + +[[tool.bumpversion.files]] +filename = "HISTORY.rst" +search = """ +--------- +""" +replace = """ +--------- + +{new_version} ({now:%d.%m.%Y}) +~~~~~~~~~~~~~~~~~~~ +""" + [tool.mypy] -python_version = 3.8 +python_version = 3.9 follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true @@ -38,27 +62,192 @@ disallow_any_generics = true disallow_untyped_defs = true mypy_path = "$MYPY_CONFIG_FILE_DIR" -[[tool.mypy.overrides]] -module = [] -ignore_missing_imports = true +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] +src = ["src", "tests"] +include = [ + "pyproject.toml", + "sedate/**/*.py", + "tests/**/*.py", + "stubs/**/*.pyi" +] +line-length = 79 +indent-width = 4 +target-version = "py39" + +[tool.ruff.lint] +select = [ + "B0", + "B904", + "B909", + "C4", + "COM818", + "D2", + "D301", + "D4", + "E", + "F", + "FLY002", + "I002", + "ISC", + "N", + "PERF", + "PGH004", + "PIE", + "PYI", + "Q", + "RUF", + "SIM", + "SLOT", + "UP", + "W" +] +ignore = [ + "B007", + "C420", + "D200", + "D201", + "D202", + "D204", + "D205", + "D209", + "D210", + "D211", + "D400", + "D401", + "D412", + "E226", + "E402", + "E711", + "E712", + "E741", + "N818", + "PYI019", + "PYI041", + "RUF012", + "RUF013", + "RUF021", + "RUF022", + "RUF023", + "RUF031", + "RUF052", + "RUF056", + "SIM103", + "SIM105", + "SIM108", + "SIM110", + "SIM118", + "SIM210", + "SIM910", + "UP009", + "UP012", + "UP032", + "UP038", +] +unfixable = [] +external = ["TC"] +allowed-confusables = ["×"] +preview = true + +[tool.ruff.lint.extend-per-file-ignores] +"tests/**/*.py" = [ + "C4", + "D", + "FLY002", + "I002", + "ISC", + "N", + "Q", + "PERF", + "PGH", + "PIE", + "PYI", + "RUF", + "SIM", + "UP", +] + +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.pep8-naming] +extend-ignore-names = [ + "afterFlowable", + "HSTORE", + "sortKey", + "URL", + "UUID" +] +classmethod-decorators = [ + # NOTE: We can potentially get rid some of these with SQLAlchemy 2.0 + # since they should cleanly combine with classmethod + "declared_attr", + "expression", + "comparator", +] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" +ignore-decorators = ["typing.overload"] + +[tool.ruff.lint.flake8-quotes] +avoid-escape = true +docstring-quotes = "double" +inline-quotes = "single" +multiline-quotes = "double" + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "lf" +docstring-code-format = true +docstring-code-line-length = "dynamic" [tool.tox] legacy_tox_ini = """ [tox] isolated_build = True -envlist = py37,py38,py39,py310,lint,bandit,mypy +envlist = py39,py310,py311,py312,py313,lint,bandit,mypy [gh-actions] python = - 3.8: py38,flake8,bandit,mypy 3.9: py39 3.10: py310 - 3.11: py310 - 3.12: py310 + 3.11: py311,flake8,bandit,mypy + 3.12: py312 + 3.13: py313 [testenv] setenv = - py{38,39,310,311,312}: COVERAGE_FILE = .coverage.{envname} + py{39,310,311,312,313}: COVERAGE_FILE = .coverage.{envname} deps = coverage[toml] pytest @@ -67,20 +256,29 @@ deps = commands = py.test --cov={envsitepackagesdir}/sedate --cov-report= {posargs} [testenv:lint] -basepython = python3.8 +basepython = python3.11 deps = flake8 -commands = flake8 sedate/ tests/ + flake8-type-checking + ruff +commands = + ruff check + flake8 sedate/ tests/ [testenv:mypy] -basepython = python3.8 +basepython = python3.11 deps = mypy types-pytz -commands = mypy -p sedate +commands = + mypy -p sedate --python-version 3.9 + mypy -p sedate --python-version 3.10 + mypy -p sedate --python-version 3.11 + mypy -p sedate --python-version 3.12 + mypy -p sedate --python-version 3.13 [testenv:bandit] -basepython = python3.8 +basepython = python3.11 deps = bandit[toml] commands = bandit -q -c pyproject.toml -r sedate/ diff --git a/sedate/__init__.py b/sedate/__init__.py index 816ee25..89b3884 100644 --- a/sedate/__init__.py +++ b/sedate/__init__.py @@ -193,7 +193,7 @@ def is_whole_day( @overload def overlaps(start: datetime, end: datetime, otherstart: datetime, otherend: datetime) -> bool: ... -@overload # noqa: E302 +@overload def overlaps(start: date_t, end: date_t, otherstart: date_t, otherend: date_t) -> bool: ... @@ -218,7 +218,7 @@ def overlaps( @overload def count_overlaps(dates: Iterable[tuple[datetime, datetime]], start: datetime, end: datetime) -> int: ... -@overload # noqa: E302 +@overload def count_overlaps(dates: Iterable[tuple[date_t, date_t]], start: date_t, end: date_t) -> int: ... @@ -307,7 +307,7 @@ def align_range_to_day( def align_date_to_week( date: datetime, timezone: TzInfoOrName, - direction: 'Direction' + direction: Direction ) -> datetime: """ Like :func:`align_date_to_day`, but for weeks. @@ -537,22 +537,24 @@ def dtrange( # the absolute order of the datetimes. It would probably be # easier/smarter to use dateutil at that point tzinfo = None - if isinstance(start, datetime): - if isinstance(start.tzinfo, pytz.tzinfo.DstTzInfo): - # we want the underspecified version that doesn't know yet - # whether - tzinfo = start.tzinfo - - # we convert to a tz-naive datetimes - start = start.replace(tzinfo=None) - if isinstance(end, datetime): - # before we convert the end to naive we need to make - # sure they're not in completely different timezones - assert isinstance(end.tzinfo, pytz.BaseTzInfo) - if end.tzinfo.zone != tzinfo.zone: - end = to_timezone(end, tzinfo) - - end = end.replace(tzinfo=None) + if ( + isinstance(start, datetime) + and isinstance(start.tzinfo, pytz.tzinfo.DstTzInfo) + ): + # we need to remember the original timezone in case + # we need to convert the end timezone + tzinfo = start.tzinfo + + # we convert to a tz-naive datetime + start = start.replace(tzinfo=None) + if isinstance(end, datetime): + # before we convert the end to naive we need to make + # sure they're not in completely different timezones + assert isinstance(end.tzinfo, pytz.BaseTzInfo) + if end.tzinfo.zone != tzinfo.zone: + end = to_timezone(end, tzinfo) + + end = end.replace(tzinfo=None) if start <= end: remaining = operator.le diff --git a/sedate/types.py b/sedate/types.py index 672f9bb..9179982 100644 --- a/sedate/types.py +++ b/sedate/types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import pytz diff --git a/setup.cfg b/setup.cfg index 36835ed..f4de812 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,11 +15,11 @@ classifiers = License :: OSI Approved :: GNU General Public License v2 (GPLv2) Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Programming Language :: Python :: Implementation :: CPython Intended Audience :: Developers Topic :: Software Development :: Libraries :: Python Modules @@ -29,15 +29,23 @@ include_package_data = True zip_safe = False packages = sedate -python_requires = >=3.8 +python_requires = >=3.9 install_requires = pytz [options.extras_require] dev = - bump2version + bump-my-version + flake8 + flake8-type-checking + mypy pre-commit + pre-commit-uv + ruff tox + tox-uv + types-pytz + uv [options.package_data] sedate = @@ -45,3 +53,9 @@ sedate = [bdist_wheel] universal=1 + +[flake8] +select = TC0,TC1 +per_file_ignores = + *.pyi: TC + tests/**.py: TC