Skip to content

v1.3.0 - #68

Open
gtkacz wants to merge 50 commits into
mainfrom
dev
Open

v1.3.0#68
gtkacz wants to merge 50 commits into
mainfrom
dev

Conversation

@gtkacz

@gtkacz gtkacz commented Aug 5, 2025

Copy link
Copy Markdown
Owner

PR Type

Enhancement, Tests, Documentation


Description

• Added comprehensive new functionality including ExtendedTimeDelta class with months/years support, time operations module, and absolute date operations
• Enhanced weekday operations with new which_of_month and which_of_year methods for determining weekday occurrences
• Modernized codebase with pipe union type hints (|) and improved formatting consistency
• Added extensive test coverage for all new modules and functionality
• Implemented comprehensive documentation system with Sphinx, dark theme CSS, and automated API generation
• Enhanced CI/CD with Python 3.13 support, package quality workflows, security scanning, and pre-commit hooks
• Improved development tooling with Windows batch scripts, enhanced Makefile targets, and comprehensive linting rules
• Added project governance with code of conduct and updated dependency management


Diagram Walkthrough

flowchart LR
  A["Core Library"] --> B["New Modules"]
  B --> C["ExtendedTimeDelta"]
  B --> D["Time Operations"]
  B --> E["Absolute Date Operations"]
  B --> F["Enhanced Weekday Operations"]
  
  A --> G["Testing & Quality"]
  G --> H["Comprehensive Test Suites"]
  G --> I["CI/CD Enhancements"]
  G --> J["Security Scanning"]
  
  A --> K["Documentation"]
  K --> L["Sphinx Integration"]
  K --> M["Dark Theme"]
  K --> N["Auto API Generation"]
  
  A --> O["Development Tools"]
  O --> P["Enhanced Pre-commit"]
  O --> Q["Windows Support"]
  O --> R["Comprehensive Linting"]
Loading

File Walkthrough

Relevant files
Enhancement
9 files
weekday_operations.py
Modernize type hints and add weekday occurrence methods   

temporal_adjuster/modules/weekday_operations.py

• Updated type hints from Union syntax to modern pipe union syntax (|)

• Standardized docstring indentation from spaces to tabs
• Added new
methods which_of_month and which_of_year for determining weekday
occurrences
• Improved code formatting and line breaks for better
readability

+175/-121
__extended_timedelta.py
Add ExtendedTimeDelta class with months and years support

temporal_adjuster/common/types/__extended_timedelta.py

• Added new ExtendedTimeDelta class extending Python's timedelta with
months and years support
• Implemented comprehensive arithmetic
operations, comparisons, and conversion methods
• Added support for
fractional time units and custom days-per-month/year configurations

Included serialization support and various utility methods for time
calculations

+712/-0 
time_operations.py
Add comprehensive time operations module                                 

temporal_adjuster/modules/time_operations.py

• Added new _TimeAdjuster class with time manipulation methods

Implemented time difference calculation, range checking, rounding, and
conversion utilities
• Added support for handling time objects that
cross midnight boundaries

+131/-0 
absolute_date_operations.py
Add absolute date operations module                                           

temporal_adjuster/modules/absolute_date_operations.py

• Added new _AbsoluteDateOperations class for date-integer conversions

• Implemented methods for converting between dates and
day-of-year/month integers
• Added comprehensive docstrings with
examples for all methods

+88/-0   
sequence_processor.py
Enhanced type annotations and documentation for sequence processor
decorator

temporal_adjuster/common/decorators/sequence_processor.py

• Improves type annotations with ParamSpec and proper generic types

Adds comprehensive docstring explaining the decorator's functionality

• Refactors logic flow for better readability and maintainability

Adds compatibility import for ParamSpec from typing_extensions

+22/-16 
temporal_adjuster.py
Extended TemporalAdjuster class with new module integrations

temporal_adjuster/temporal_adjuster.py

• Adds imports for new module classes (_AbsoluteDateOperations,
_TimeAdjuster)
• Extends class inheritance to include additional
functionality modules
• Updates class docstring formatting and removes
unnecessary pass statement

+11/-5   
dates.py
Enhanced type definitions with time types and documentation

temporal_adjuster/common/types/dates.py

• Adds comprehensive module docstring and type documentation

Introduces new time-related type aliases (AnyTime, TimeT)
• Improves
existing type annotations with proper documentation comments

+12/-1   
__init__.py
Extended module exports with new operation classes             

temporal_adjuster/modules/init.py

• Adds imports for new module classes (_AbsoluteDateOperations,
_TimeAdjuster)
• Expands module interface to include additional
functionality

+2/-0     
__init__.py
Extended type exports with time types and ExtendedTimeDelta

temporal_adjuster/common/types/init.py

• Adds import for new ExtendedTimeDelta class
• Includes new
time-related type exports (AnyTime, TimeT)

+2/-1     
Tests
4 files
test_weekday_operations.py
Add tests for new weekday occurrence methods                         

tests/test_weekday_operations.py

• Updated test formatting with consistent trailing commas and line
breaks
• Added comprehensive tests for new which_of_month and
which_of_year methods
• Improved test readability with better
parameter formatting

+130/-44
test_time_operations.py
Add comprehensive time operations test suite                         

tests/test_time_operations.py

• Added comprehensive test suite for time operations functionality

Includes tests for time difference calculations, range checking,
rounding, and conversions
• Tests cover both single values and
sequence operations with edge cases

+260/-0 
test_absolute_date_operations.py
Add comprehensive absolute date operations test suite       

tests/test_absolute_date_operations.py

• Added comprehensive test suite for absolute date operations
• Tests
cover date-to-integer conversions and integer-to-date conversions

Includes tests for both single values and sequences with edge cases

+152/-0 
test_extended_timedelta.py
Add comprehensive ExtendedTimeDelta test suite                     

tests/test_extended_timedelta.py

• Added comprehensive test suite for the new ExtendedTimeDelta class

Tests cover initialization, arithmetic operations, comparisons, and
conversions
• Includes tests for edge cases, serialization, and custom
configuration options

+150/-0 
Formatting
5 files
first_and_last_day_operations.py
Standardize docstring formatting and improve code style   

temporal_adjuster/modules/first_and_last_day_operations.py

• Standardized docstring indentation from spaces to tabs
• Added
trailing commas to function calls for consistency
• Minor formatting
improvements for better code readability

+66/-48 
test_first_and_last_days.py
Improve test formatting and readability                                   

tests/test_first_and_last_days.py

• Updated test formatting with consistent trailing commas and line
breaks
• Improved test readability with better parameter formatting

Standardized test method call formatting across all test cases

+28/-23 
test_sequenceable.py
Code formatting improvements and modernization in sequenceable tests

tests/test_sequenceable.py

• Removes redundant comments and improves code formatting
• Replaces
set([...]) syntax with modern set literal syntax {...}
• Adds trailing
commas for better code consistency

+6/-8     
test_performance.py
Code formatting improvements in performance tests               

tests/test_performance.py

• Simplifies list comprehension formatting for better readability

Removes redundant comment lines
• Improves code consistency with
trailing comma addition

+2/-7     
setup.py
Minor cleanup in setup.py file handling                                   

setup.py

• Removes unnecessary 'r' mode specifier when opening files for
reading
• Minor code cleanup for file handling operations

+2/-2     
Documentation
15 files
conf.py
Enhanced Sphinx documentation configuration with dynamic metadata
loading

docs/conf.py

• Dynamically loads project metadata from pyproject.toml using tomllib

• Adds new Sphinx extensions (viewcode, napoleon) and configures
Napoleon settings
• Changes HTML theme to sphinx_rtd_theme and adds
dark CSS styling
• Configures comprehensive autodoc settings for
better documentation generation

+52/-10 
dark.css
Complete dark theme CSS implementation for documentation 

docs/_static/css/dark.css

• Adds comprehensive dark theme CSS for documentation
• Implements
dark mode styling for all documentation elements
• Includes syntax
highlighting and UI component theming

+1996/-0
README.md
Added Snyk package quality badge to README                             

README.md

• Adds new Snyk package quality badge to project status indicators

+1/-2     
index.rst
Enhanced documentation homepage with examples and better structure

docs/index.rst

• Enhances documentation homepage with project description and quick
examples
• Adds practical code examples demonstrating library usage

Improves table of contents structure and navigation

+32/-2   
CODE_OF_CONDUCT.md
Added project code of conduct guidelines                                 

CODE_OF_CONDUCT.md

• Establishes project code of conduct with emphasis on professionalism
and neutrality
• Provides guidelines for inclusive and respectful
contribution practices

+26/-0   
temporal_adjuster.modules.rst
Auto-generated API documentation for modules package         

docs/temporal_adjuster.modules.rst

• Auto-generated Sphinx documentation file for modules package

Provides comprehensive API documentation structure for all module
components

+45/-0   
ta.rst
Restructured main API documentation with comprehensive sections

docs/ta.rst

• Restructures main documentation page with detailed API reference

Adds comprehensive sections for types, enums, exceptions, and
utilities
• Improves documentation organization and navigation

+41/-5   
CHANGELOG.md
Updated changelog with version 1.3.0 feature additions     

CHANGELOG.md

• Documents new features for version 1.3.0 including day_of_year
method
• Adds information about new weekday methods and numpy
vectorization improvements

+8/-0     
temporal_adjuster.rst
Auto-generated main package API documentation                       

docs/temporal_adjuster.rst

• Auto-generated Sphinx documentation file for main temporal_adjuster
package
• Provides structured API documentation with subpackages and
modules

+30/-0   
temporal_adjuster.common.enums.rst
Auto-generated API documentation for common enums               

docs/temporal_adjuster.common.enums.rst

• Auto-generated Sphinx documentation for common enums package

Documents day_of_week module and package structure

+21/-0   
temporal_adjuster.common.decorators.rst
Auto-generated API documentation for common decorators     

docs/temporal_adjuster.common.decorators.rst

• Auto-generated Sphinx documentation for common decorators package

Documents sequence_processor module and package structure

+21/-0   
temporal_adjuster.common.types.rst
Auto-generated API documentation for common types               

docs/temporal_adjuster.common.types.rst

• Auto-generated Sphinx documentation for common types package

Documents dates module and package structure

+21/-0   
temporal_adjuster.common.exceptions.rst
Auto-generated API documentation for common exceptions     

docs/temporal_adjuster.common.exceptions.rst

• Auto-generated Sphinx documentation for common exceptions package

Documents common exceptions module and package structure

+21/-0   
temporal_adjuster.common.rst
Auto-generated API documentation for common package           

docs/temporal_adjuster.common.rst

• Auto-generated Sphinx documentation for common package
• Provides
comprehensive structure for all common subpackages

+21/-0   
modules.rst
Auto-generated root documentation file for modules             

docs/modules.rst

• Auto-generated Sphinx documentation root file for module structure

Provides entry point for comprehensive API documentation

+7/-0     
Configuration changes
11 files
CI.yml
CI workflow updates with Python 3.13 support and streamlined testing

.github/workflows/CI.yml

• Adds Python 3.13 support across all operating systems
• Updates
GitHub Actions versions and dependency file references
• Removes
coverage reporting and threshold checking from CI workflow

+7/-14   
package_quality.yml
New comprehensive package quality assurance workflow         

.github/workflows/package_quality.yml

• Creates comprehensive package quality workflow with multiple checks

• Includes Ruff formatting, coverage testing, security scanning, and
leak detection
• Integrates Snyk security analysis and CodeQL scanning

+67/-0   
.pre-commit-config.yaml
Enhanced pre-commit configuration with security and quality checks

.pre-commit-config.yaml

• Updates all hook versions to latest releases
• Adds new hooks for
security (gitleaks), type checking (mypy), and code quality
• Includes
documentation building and project validation hooks

+25/-3   
make.bat
Windows batch script for development commands                       

make.bat

• Creates Windows batch script equivalent of Makefile
• Provides
commands for testing, building, documentation, and development setup

+65/-0   
ruff.toml
Comprehensive Ruff configuration with extensive linting rules

ruff.toml

• Significantly expands linting rules to include comprehensive code
quality checks
• Configures advanced formatting options and per-file
rule exceptions
• Updates target Python version and adds preview
features

+23/-5   
readthedocs.yml
Automated Sphinx documentation building and publishing workflow

.github/workflows/readthedocs.yml

• Creates automated workflow for building and publishing Sphinx
documentation
• Configures automatic documentation updates on main
branch pushes

+41/-0   
make.bat
Added API documentation generation command to Windows build script

docs/make.bat

• Adds apidoc command for generating Sphinx API documentation

Extends Windows documentation build capabilities

+4/-0     
.readthedocs.yaml
Updated Read the Docs configuration with modern versions 

.readthedocs.yaml

• Updates Read the Docs configuration to Ubuntu 24.04 and Python 3.12

• Restructures configuration for better documentation building

+15/-5   
Makefile
Enhanced Makefile with setup target and improved testing 

Makefile

• Adds new setup target for development environment initialization

Includes coverage reporting in test target and improves build process

+8/-0     
Makefile
Added API documentation generation target to docs Makefile

docs/Makefile

• Adds apidoc target for generating Sphinx API documentation
• Extends
documentation build capabilities with automated API doc generation

+4/-0     
pyproject.toml
Added type information package data configuration               

pyproject.toml

• Adds setuptools package data configuration for type information
files

+3/-0     
Dependencies
2 files
requirements.dev.txt
Reorganized development dependencies with security updates

requirements.dev.txt

• Reorganizes development dependencies and adds numpy as direct
requirement
• Updates security-pinned package versions for
vulnerability mitigation

+3/-2     
requirements.txt
Documentation build requirements specification                     

docs/requirements.txt

• Specifies documentation build dependencies including Sphinx and
theme
• Ensures consistent documentation building environment

+4/-0     
Bug fix
1 files
publish.yml
Fixed dependency file reference in publish workflow           

.github/workflows/publish.yml

• Updates dependency file reference from requirements_dev.txt to
requirements.dev.txt

+1/-1     
Additional files
8 files
ruff.yml +0/-10   
.gitkeep [link]   
sequence_processor.pyi +0/-8     
day_of_week.pyi +0/-33   
common.pyi +0/-1     
dates.pyi +0/-5     
business_day_operations.py [link]   
temporal_adjuster.pyi +0/-562 

gtkacz and others added 30 commits June 17, 2024 16:35
* Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.0 (#27)

Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](pypa/gh-action-pypi-publish@v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1 (#28)

Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](pypa/gh-action-pypi-publish@v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@gtkacz gtkacz added this to the v1.3.0 milestone Aug 5, 2025
@gtkacz
gtkacz requested a review from Copilot August 5, 2025 02:02
@gtkacz gtkacz self-assigned this Aug 5, 2025
@sourcery-ai

sourcery-ai Bot commented Aug 5, 2025

Copy link
Copy Markdown

Reviewer's Guide

This PR implements v1.3.0 by extending TemporalAdjuster with new absolute date and time adjusters, introduces ExtendedTimeDelta for month/year support, enhances weekday operations with occurrence queries and modern type hints, refines the sequenceable decorator, and updates project tooling and configurations for documentation, linting, and CI/CD.

Class diagram for new and updated date/time adjusters

classDiagram
    class TemporalAdjuster {
        <<inherits>>
    }
    class _AbsoluteDateOperations {
        +int_to_day_of_year(date, int_value) DateT
        +int_to_day_of_month(date, int_value) DateT
        +date_to_int_of_year(date) int
        +date_to_int_of_month(date) int
    }
    class _TimeAdjuster {
        +time_difference(time_obj_1, time_obj_2) ExtendedTimeDelta
        +is_time_in_range(time_obj, start, end) bool
        +round_time(time_obj, round_to) TimeT
        +time_to_seconds(time_obj) float
        +seconds_to_time(seconds) time
    }
    class _TemporalAdjusterForWeekday {
        +which_of_month(weekday, date) int
        +which_of_year(weekday, date) int
        ...
    }
    class ExtendedTimeDelta {
        +months: int
        +years: int
        +to_timedelta() timedelta
        +to_microseconds() float
        +to_seconds() float
        +to_minutes() float
        +to_hours() float
        +to_days() float
        +to_weeks() float
        +to_months() float
        +to_years() float
    }
    TemporalAdjuster <|-- _AbsoluteDateOperations
    TemporalAdjuster <|-- _TemporalAdjusterForFirstAndLastDays
    TemporalAdjuster <|-- _TemporalAdjusterForWeekday
    TemporalAdjuster <|-- _TimeAdjuster
    _TimeAdjuster ..> ExtendedTimeDelta : uses
    _AbsoluteDateOperations <.. TemporalAdjuster : inherited
    _TemporalAdjusterForWeekday <.. TemporalAdjuster : inherited
    _TimeAdjuster <.. TemporalAdjuster : inherited
    ExtendedTimeDelta <|-- timedelta
Loading

Class diagram for the updated sequenceable decorator

classDiagram
    class sequenceable {
        +__call__(func: Callable[P, R]) -> Callable[P, R | Any]
    }
    sequenceable o-- Callable
    sequenceable o-- numpy.ndarray
Loading

Class diagram for new and updated date/time type aliases

classDiagram
    class AnyDate
    class DateT
    class AnyTime
    class TimeT
    AnyDate <|-- datetime
    AnyDate <|-- date
    AnyTime <|-- datetime
    AnyTime <|-- time
    DateT <|-- AnyDate
    TimeT <|-- AnyTime
Loading

Class diagram for ExtendedTimeDelta

classDiagram
    class ExtendedTimeDelta {
        +months: int
        +years: int
        +from_timedelta(td: timedelta) ExtendedTimeDelta
        +to_timedelta() timedelta
        +to_microseconds() float
        +to_seconds() float
        +to_minutes() float
        +to_hours() float
        +to_days() float
        +to_weeks() float
        +to_months() float
        +to_years() float
    }
    ExtendedTimeDelta <|-- timedelta
Loading

File-Level Changes

Change Details Files
New time-based adjusters added
  • Added time_operations module with sequenceable methods for time_difference, is_time_in_range, round_time, time_to_seconds, and seconds_to_time
  • Integrated _TimeAdjuster into the TemporalAdjuster class
  • Introduced comprehensive tests for time operations
temporal_adjuster/modules/time_operations.py
temporal_adjuster/temporal_adjuster.py
tests/test_time_operations.py
Absolute date adjusters introduced
  • Created absolute_date_operations module with int_to_day_of_year, int_to_day_of_month, date_to_int_of_year, and date_to_int_of_month methods
  • Extended TemporalAdjuster to include _AbsoluteDateOperations base
  • Added tests for absolute date operations
temporal_adjuster/modules/absolute_date_operations.py
temporal_adjuster/temporal_adjuster.py
tests/test_absolute_date_operations.py
ExtendedTimeDelta supports months and years
  • Implemented ExtendedTimeDelta class with months/years support and arithmetic/conversion methods
  • Updated common type definitions to include AnyDate, AnyTime, and ExtendedTimeDelta
  • Added tests covering initialization, arithmetic, comparisons, conversions, and serialization
temporal_adjuster/common/types/__extended_timedelta.py
temporal_adjuster/common/types/dates.py
tests/test_extended_timedelta.py
Weekday adjusters enhanced with occurrence queries and refactoring
  • Refactored weekday_operations to use modern type hints (PEP 604) and cleaned up formatting
  • Added which_of_month and which_of_year methods to query the occurrence of a weekday
  • Updated corresponding tests to cover new methods and consistent argument formatting
temporal_adjuster/modules/weekday_operations.py
tests/test_weekday_operations.py
Sequence decorator typing and logic improvements
  • Reworked sequence_processor decorator to use ParamSpec for accurate typing
  • Simplified wrapper logic while preserving sequence handling
  • Adjusted sequenceable tests for formatting consistency
temporal_adjuster/common/decorators/sequence_processor.py
tests/test_sequenceable.py
Project configuration and tooling updated
  • Revised Sphinx and ReadTheDocs configs to load settings from pyproject and apply a custom dark theme
  • Upgraded pre-commit hooks and ruff configuration (line lengths, ignores, new hooks)
  • Expanded CI workflows for additional Python versions and added package_quality and docs build jobs
  • Updated Makefiles, setup.py, pyproject.toml, CODE_OF_CONDUCT, README badge, and requirements file naming
docs/conf.py
.readthedocs.yaml
docs/_static/css/dark.css
.pre-commit-config.yaml
ruff.toml
.github/workflows/CI.yml
.github/workflows/package_quality.yml
.github/workflows/publish.yml
.github/workflows/readthedocs.yml
Makefile
make.bat
setup.py
pyproject.toml
CODE_OF_CONDUCT.md
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR focuses on updating the Temporal Adjuster project to version 1.3.0, adding significant new functionality while maintaining code quality and consistency. The changes primarily involve adding time operations, absolute date operations, enhanced types, and improving code formatting across the codebase.

  • Added new time operations module with methods for time calculations, rounding, and range checks
  • Added absolute date operations for converting between dates and integer representations
  • Enhanced the ExtendedTimeDelta class with comprehensive date/time duration support
  • Updated formatting and linting configuration with more comprehensive rules

Reviewed Changes

Copilot reviewed 53 out of 56 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_*.py Added comprehensive test coverage for new functionality while updating formatting
temporal_adjuster/temporal_adjuster.py Integrated new module classes into main TemporalAdjuster class
temporal_adjuster/modules/ Added new time_operations and absolute_date_operations modules
temporal_adjuster/common/types/ Enhanced type system with ExtendedTimeDelta and additional time types
docs/ Updated documentation structure and configuration
ruff.toml Modernized linting configuration with stricter rules

Comment thread tests/test_time_operations.py
Comment on lines +5 to +11
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
with:
args: "format --check"
coverage:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 10 months ago

To fix the problem, we need to add a permissions block setting explicit, least-privilege permissions for the jobs that currently have no such block: specifically, add this at the job level (ruff, coverage, and security) or at the workflow root to apply by default to all jobs. The best and simplest fix is to add a top-level permissions block to the workflow, immediately after the workflow name and event trigger block, setting contents: read. This will make all jobs default to only read access (except where overridden, like scan), meeting the CodeQL recommendation and GitHub security best practices. No changes to individual job internals or steps are required. If later, a job needs more permissions, add or override as needed.

Specifically:

  • In .github/workflows/package_quality.yml, insert the following block after the name: and before on: or directly after on: ...:
    permissions:
      contents: read

This will apply least-privilege permissions globally, except for jobs with an explicit block (scan).

No new imports, methods, or definitions are needed outside of this YAML edit.

Suggested changeset 1
.github/workflows/package_quality.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/package_quality.yml b/.github/workflows/package_quality.yml
--- a/.github/workflows/package_quality.yml
+++ b/.github/workflows/package_quality.yml
@@ -1,4 +1,6 @@
 name: Package quality
+permissions:
+  contents: read
 on: [push, pull_request]
 jobs:
   ruff:
EOF
@@ -1,4 +1,6 @@
name: Package quality
permissions:
contents: read
on: [push, pull_request]
jobs:
ruff:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +12 to +42
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5.2.0
with:
python-version: 3.12
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install coverage
pip install -r requirements.dev.txt
- name: Check coverage threshold
run: |
coverage run -m unittest discover tests/ -v
coverage report --fail-under=95

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}

- name: Check type completeness
uses: gtkacz/pyanalyze-action@v1

- name: Check for dead code
uses: gtkacz/vulture-action@1.0.0
with:
args: '--min-confidence 70 --exclude "*/docs/*,setup.py"'

security:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 10 months ago

To resolve the problem, add an explicit permissions block limiting GITHUB_TOKEN permissions to the least privilege required. Since there is already a permissions block within the scan job, the best approach is to set a root-level permissions block so that all jobs get limited permissions by default, and specific jobs can override if needed. Based on best practices and the provided recommendation, add the following block at the top level (below name but above on):

permissions:
  contents: read

This ensures that all jobs in the workflow inherit contents: read as the minimal required permission for accessing repository content, unless they explicitly specify different permissions (as scan does). No further methods, imports, or definitions are needed.

Suggested changeset 1
.github/workflows/package_quality.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/package_quality.yml b/.github/workflows/package_quality.yml
--- a/.github/workflows/package_quality.yml
+++ b/.github/workflows/package_quality.yml
@@ -1,4 +1,6 @@
 name: Package quality
+permissions:
+  contents: read
 on: [push, pull_request]
 jobs:
   ruff:
EOF
@@ -1,4 +1,6 @@
name: Package quality
permissions:
contents: read
on: [push, pull_request]
jobs:
ruff:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +43 to +58
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/python-3.10@master
continue-on-error: true
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --sarif-file-output=snyk.sarif
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: snyk.sarif

scan:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 10 months ago

To fix the issue, an explicit permissions block should be added. The best way is to set permissions: contents: read at the workflow root (before the jobs: section) to restrict all jobs to read-only permissions unless otherwise specified. This enforces the principle of least privilege. If a job requires additional permissions, a job-specific block should be added (as already done for the scan job). The change should be made at the very top of the file after the name: and before the on: or jobs: keys to ensure it applies to the workflow as a whole wherever not overridden.

Suggested changeset 1
.github/workflows/package_quality.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/package_quality.yml b/.github/workflows/package_quality.yml
--- a/.github/workflows/package_quality.yml
+++ b/.github/workflows/package_quality.yml
@@ -1,4 +1,6 @@
 name: Package quality
+permissions:
+  contents: read
 on: [push, pull_request]
 jobs:
   ruff:
EOF
@@ -1,4 +1,6 @@
name: Package quality
permissions:
contents: read
on: [push, pull_request]
jobs:
ruff:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread .github/workflows/package_quality.yml Fixed
@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Logic Error

The which_of_month method doesn't validate that the input date actually matches the specified weekday, potentially returning incorrect occurrence numbers for mismatched weekday-date combinations.

def which_of_month(weekday: Weekday | ISOWeekday, date: AnyDate) -> int:
	"""
	Returns the occurrence of the given day of the week in the month of the given date.

	Args:
		weekday (Weekday): The day of the week.
		date (DateT): The reference date.

	Returns:
		int: The occurrence of the given day of the week in the month of the given date.

	"""
	weekday = _TemporalAdjusterForWeekday.__normalize_weekday(weekday)

	return (date.day - 1) // 7 + 1
Type Safety

The __add__ and __sub__ methods don't return values in all code paths when handling unsupported types, which could lead to None returns instead of proper error handling.

def __add__(self, other: Union[timedelta, 'ExtendedTimeDelta']) -> Self:
	"""
	Add two ExtendedTimeDelta or timedelta objects.

	Args:
		other (ExtendedTimeDelta or timedelta): The time delta to add.

	Returns:
		ExtendedTimeDelta: A new ExtendedTimeDelta representing the sum.

	Example:
		>>> et1 = ExtendedTimeDelta(years=1, days=10)
		>>> et2 = ExtendedTimeDelta(months=6, days=5)
		>>> result = et1 + et2
		>>> result
		ExtendedTimeDelta(years=1, months=6, days=15, seconds=0, microseconds=0)

	"""
	if isinstance(other, ExtendedTimeDelta):
		return ExtendedTimeDelta(
			days=self.days + other.days,
			seconds=self.seconds + other.seconds,
			microseconds=self.microseconds + other.microseconds,
			months=self.months + other.months,
			years=self.years + other.years,
		)
	if isinstance(other, timedelta):
		parent_self = self.to_timedelta()
		parent_result = parent_self + other
		return ExtendedTimeDelta.from_timedelta(parent_result)

__radd__ = __add__

def __sub__(self, other: Union[timedelta, 'ExtendedTimeDelta']) -> Self:
	"""
	Subtract an ExtendedTimeDelta or timedelta from this ExtendedTimeDelta.

	Args:
		other (ExtendedTimeDelta or timedelta): The time delta to subtract.

	Returns:
		ExtendedTimeDelta: A new ExtendedTimeDelta representing the difference.

	Example:
		>>> et1 = ExtendedTimeDelta(years=2, months=3, days=10)
		>>> et2 = ExtendedTimeDelta(years=1, months=1, days=5)
		>>> result = et1 - et2
		>>> result
		ExtendedTimeDelta(years=1, months=2, days=5, seconds=0, microseconds=0)

	"""
	if isinstance(other, ExtendedTimeDelta):
		return ExtendedTimeDelta(
			days=self.days - other.days,
			seconds=self.seconds - other.seconds,
			microseconds=self.microseconds - other.microseconds,
			months=self.months - other.months,
			years=self.years - other.years,
		)
	if isinstance(other, timedelta):
		parent_self = self.to_timedelta()
		parent_result = parent_self - other
		return ExtendedTimeDelta.from_timedelta(parent_result)
Performance Issue

Class attributes DAYS_IN_MONTH and DAYS_IN_YEAR are set on every instance creation in __new__, which is inefficient and could cause issues with concurrent access or different instances having different conversion rates.

cls.DAYS_IN_MONTH = days_in_month
cls.DAYS_IN_YEAR = days_in_year

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @gtkacz - I've reviewed your changes - here's some feedback:

  • Add validation in int_to_day_of_year and int_to_day_of_month to ensure the provided index is within the valid range for that year or month and raise a clear error when out of bounds.
  • The which_of_month and which_of_year methods take a weekday argument but never use it to verify that the date’s weekday matches; either enforce that check or remove the unused parameter.
  • In the sequenceable decorator, converting every iterable to a NumPy array can break on non–numeric or custom sequence types—consider falling back to plain Python iteration when NumPy conversion isn’t appropriate.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Add validation in int_to_day_of_year and int_to_day_of_month to ensure the provided index is within the valid range for that year or month and raise a clear error when out of bounds.
- The which_of_month and which_of_year methods take a weekday argument but never use it to verify that the date’s weekday matches; either enforce that check or remove the unused parameter.
- In the sequenceable decorator, converting every iterable to a NumPy array can break on non–numeric or custom sequence types—consider falling back to plain Python iteration when NumPy conversion isn’t appropriate.

## Individual Comments

### Comment 1
<location> `temporal_adjuster/common/types/__extended_timedelta.py:6` </location>
<code_context>
+from typing import Self, Union
+
+
+class ExtendedTimeDelta(timedelta):
+	"""
+	An extended version of Python's timedelta that supports months and years.
</code_context>

<issue_to_address>
Subclassing timedelta may lead to subtle issues due to its C implementation.

Subclassing timedelta can cause unexpected behavior, especially when adding new fields like months and years. Consider using composition instead, or clearly document the limitations and test for edge cases.
</issue_to_address>

### Comment 2
<location> `temporal_adjuster/common/types/__extended_timedelta.py:16` </location>
<code_context>
+	are approximations and may not be suitable for all use cases.
+	"""
+
+	__slots__ = (
+		'_days',
+		'_hashcode',
+		'_microseconds',
+		'_months',
+		'_seconds',
+		'_years',
+	)
+
</code_context>

<issue_to_address>
Defining __slots__ may not be compatible with all uses of timedelta subclassing.

Subclasses of C extension types like timedelta may not safely support __slots__. Consider removing __slots__ or verify all parent classes are compatible to avoid potential bugs or crashes.
</issue_to_address>

### Comment 3
<location> `temporal_adjuster/common/types/__extended_timedelta.py:67` </location>
<code_context>
+			1 month, 15 days, 0:00:00
+
+		"""
+		cls.DAYS_IN_MONTH = days_in_month
+		cls.DAYS_IN_YEAR = days_in_year
+
+		# Process microseconds
</code_context>

<issue_to_address>
Assigning DAYS_IN_MONTH and DAYS_IN_YEAR as class variables can cause thread-safety issues.

Since these variables are shared across all instances, concurrent modifications can cause incorrect behavior. Use instance variables or immutable class constants instead.
</issue_to_address>

### Comment 4
<location> `temporal_adjuster/common/types/__extended_timedelta.py:232` </location>
<code_context>
+			parent_result = parent_self - other
+			return ExtendedTimeDelta.from_timedelta(parent_result)
+
+	def __mul__(self, other: Union[timedelta, 'ExtendedTimeDelta']) -> Self:
+		"""
+		Multiply this ExtendedTimeDelta by an integer.
</code_context>

<issue_to_address>
The __mul__ method signature and implementation do not match typical timedelta behavior.

Update the type hint and docstring to indicate that __mul__ should accept a number (int or float), not a timedelta. The implementation should raise a TypeError if 'other' is not numeric. Also, implement __rmul__ for commutative support.
</issue_to_address>

### Comment 5
<location> `temporal_adjuster/common/types/__extended_timedelta.py:497` </location>
<code_context>
+			),
+		)
+
+	def __dict__(self) -> dict:
+		"""
+		Return a dictionary representation of the ExtendedTimeDelta.
</code_context>

<issue_to_address>
Overriding __dict__ as a method is non-standard and may break introspection.

__dict__ should remain an attribute; using it as a method can cause compatibility issues. Please rename the method to something like to_dict.
</issue_to_address>

### Comment 6
<location> `temporal_adjuster/common/types/__extended_timedelta.py:367` </location>
<code_context>
+		if isinstance(other, (ExtendedTimeDelta, timedelta)):
+			return self._cmp(other) >= 0
+
+	def _cmp(self, other: Union[timedelta, 'ExtendedTimeDelta']) -> int:
+		"""
+		Compare this ExtendedTimeDelta with another time delta.
</code_context>

<issue_to_address>
The _cmp method returns a timedelta or int, which may not be consistent.

Currently, comparing with a standard timedelta returns a timedelta object instead of an int, which may break comparison operators. To ensure consistency, always return an int—consider using total_seconds or another scalar for comparison.
</issue_to_address>

### Comment 7
<location> `temporal_adjuster/modules/time_operations.py:64` </location>
<code_context>
+		if isinstance(time_obj, datetime):
+			time_obj = time_obj.time()
+
+		return start <= time_obj <= end if start <= end else time_obj >= start or time_obj <= end
+
+	@staticmethod
</code_context>

<issue_to_address>
The time range check may not handle all edge cases with equal start and end times.

When start == end, the function returns True for any time, which may not be intended. Please clarify, document, or handle this edge case explicitly.

Suggested implementation:

```python
		if isinstance(time_obj, datetime):
			time_obj = time_obj.time()

		# Handle the edge case where start == end
		# If start == end, only return True if time_obj == start (i.e., the range is a single point)
		if start == end:
			return time_obj == start

		return start <= time_obj <= end if start < end else time_obj >= start or time_obj <= end

	@staticmethod

```

```python
		"""
		Calculate if a time object falls within a time range, accounting for wrapping around midnight.

		Edge case:
			- If start == end, the range is considered a single point in time, and only time_obj == start returns True.
		"""

```
</issue_to_address>

### Comment 8
<location> `temporal_adjuster/modules/time_operations.py:110` </location>
<code_context>
+			float: The total number of seconds since midnight.
+
+		"""
+		return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second + time_obj.microsecond / 1e6
+
+	@staticmethod
</code_context>

<issue_to_address>
No type check for time_obj in time_to_seconds may cause AttributeError.

Add a type check or input validation to prevent AttributeError when time_obj is not a time or datetime instance.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
		"""
		Convert a time object to the total number of seconds since midnight.

		Args:
			time_obj (AnyTime): The time object to convert.

		Returns:
			float: The total number of seconds since midnight.

		"""
		return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second + time_obj.microsecond / 1e6
=======
		"""
		Convert a time object to the total number of seconds since midnight.

		Args:
			time_obj (AnyTime): The time object to convert.

		Returns:
			float: The total number of seconds since midnight.

		"""
		import datetime
		if not isinstance(time_obj, (datetime.time, datetime.datetime)):
			raise TypeError(
				f"time_obj must be an instance of datetime.time or datetime.datetime, got {type(time_obj)}"
			)
		return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second + time_obj.microsecond / 1e6
>>>>>>> REPLACE

</suggested_fix>

### Comment 9
<location> `temporal_adjuster/modules/time_operations.py:131` </location>
<code_context>
+		minute = int(seconds // 60)
+		seconds %= 60
+
+		return time(hour, minute, int(seconds), int((seconds - int(seconds)) * 1e6))
</code_context>

<issue_to_address>
Possible floating point rounding issues in seconds_to_time.

The microseconds calculation can yield inaccurate or negative values due to floating point errors. Use round() or clamp the result to [0, 999999] to ensure correctness.
</issue_to_address>

### Comment 10
<location> `tests/test_absolute_date_operations.py:145` </location>
<code_context>
+
+	def test_edge_cases(self):
+		"""Test edge cases and potential error conditions."""
+		result = TemporalAdjuster.int_to_day_of_year(date(2020, 1, 1), 60)
+		self.assertEqual(result, date(2020, 2, 29))
+
+		result = TemporalAdjuster.date_to_int_of_year([])
+		self.assertEqual(result, [])
+
+		result = TemporalAdjuster.date_to_int_of_month([date(2021, 5, 20)])
+		self.assertEqual(result, [20])
</code_context>

<issue_to_address>
Consider adding tests for invalid or out-of-range day numbers.

Please add tests for invalid day numbers (e.g., 0, negative, or exceeding valid range) to verify proper error handling in these methods.
</issue_to_address>

### Comment 11
<location> `tests/test_extended_timedelta.py:45` </location>
<code_context>
+		self.assertEqual(et.days, 1)
+		self.assertEqual(et.seconds, 43200)
+
+	def test_initialization_negative_days(self):
+		et = ExtendedTimeDelta(days=-5, days_in_month=30)
+		self.assertEqual(et.years, -1)
+		self.assertEqual(et.months, 11)
+		self.assertEqual(et.days, 25)
+
+	def test_properties(self):
+		et = ExtendedTimeDelta(years=3, months=4, days=5, seconds=6, microseconds=7)
+		self.assertEqual(et.years, 3)
</code_context>

<issue_to_address>
Consider adding tests for invalid or extreme values in ExtendedTimeDelta.

Please add tests for negative, very large, and non-integer values, as well as for conflicting or ambiguous arguments, to ensure robust handling of all input scenarios.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
	def test_properties(self):
		et = ExtendedTimeDelta(years=3, months=4, days=5, seconds=6, microseconds=7)
		self.assertEqual(et.years, 3)
		self.assertEqual(et.months, 4)
		self.assertEqual(et.days, 5)
		self.assertEqual(et.seconds, 6)
		self.assertEqual(et.microseconds, 7)
=======
	def test_properties(self):
		et = ExtendedTimeDelta(years=3, months=4, days=5, seconds=6, microseconds=7)
		self.assertEqual(et.years, 3)
		self.assertEqual(et.months, 4)
		self.assertEqual(et.days, 5)
		self.assertEqual(et.seconds, 6)
		self.assertEqual(et.microseconds, 7)

	def test_invalid_non_integer_values(self):
		with self.assertRaises(TypeError):
			ExtendedTimeDelta(years="one")
		with self.assertRaises(TypeError):
			ExtendedTimeDelta(months=[1, 2])
		with self.assertRaises(TypeError):
			ExtendedTimeDelta(days={"days": 1})
		with self.assertRaises(TypeError):
			ExtendedTimeDelta(seconds=None)
		with self.assertRaises(TypeError):
			ExtendedTimeDelta(microseconds="1000")

	def test_large_values(self):
		et = ExtendedTimeDelta(years=10**6, months=10**6, days=10**6, seconds=10**6, microseconds=10**6)
		self.assertEqual(et.years, 10**6)
		self.assertEqual(et.months, 10**6)
		self.assertEqual(et.days, 10**6)
		self.assertEqual(et.seconds, 10**6)
		self.assertEqual(et.microseconds, 10**6)

	def test_conflicting_arguments(self):
		# Assuming ExtendedTimeDelta should handle negative and positive values together
		et = ExtendedTimeDelta(years=1, months=-2, days=30, seconds=-60)
		self.assertEqual(et.years, 1)
		self.assertEqual(et.months, -2)
		self.assertEqual(et.days, 30)
		self.assertEqual(et.seconds, -60)

	def test_non_integer_values(self):
		et = ExtendedTimeDelta(years=1.5, months=2.7, days=3.9, seconds=4.1, microseconds=5.6)
		self.assertEqual(et.years, 1)
		self.assertEqual(et.months, 2)
		self.assertEqual(et.days, 3)
		self.assertEqual(et.seconds, 4)
		self.assertEqual(et.microseconds, 5)
>>>>>>> REPLACE

</suggested_fix>

### Comment 12
<location> `CHANGELOG.md:15` </location>
<code_context>
+### Added
+
+- New `day_of_year` method, that returns an integer ranging from 1-365 (or 366 for leap years) corresponding to the day of the year.
+- New `which_of_month` and `which_of_year` weekday methods, that return an integer representing which occurence of a weekday in a month or year a given date falls on. For example, `which_of_month` will return 1 for the first Monday of the month, 2 for the second Monday, and so on. Similarly, `which_of_year` will return 1 for the first Monday of the year, 2 for the second Monday, and so on.
+- Altered sequence processing to make use of numpy vectorization.
+
</code_context>

<issue_to_address>
Typo: 'occurence' should be 'occurrence'.

Update the changelog to use the correct spelling: 'occurrence'.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
- New `which_of_month` and `which_of_year` weekday methods, that return an integer representing which occurence of a weekday in a month or year a given date falls on. For example, `which_of_month` will return 1 for the first Monday of the month, 2 for the second Monday, and so on. Similarly, `which_of_year` will return 1 for the first Monday of the year, 2 for the second Monday, and so on.
=======
- New `which_of_month` and `which_of_year` weekday methods, that return an integer representing which occurrence of a weekday in a month or year a given date falls on. For example, `which_of_month` will return 1 for the first Monday of the month, 2 for the second Monday, and so on. Similarly, `which_of_year` will return 1 for the first Monday of the year, 2 for the second Monday, and so on.
>>>>>>> REPLACE

</suggested_fix>

### Comment 13
<location> `temporal_adjuster/common/decorators/sequence_processor.py:17` </location>
<code_context>


-def sequenceable(target: str):
+def sequenceable(target: str) -> Callable[[Callable[P, R]], Callable[P, R | Any]]:
 	"""
-	This decorator is used to process if a sequence of values passed as an argument to a function. The function is called for each value in the sequence, and the result is stored in the same position in the sequence.
</code_context>

<issue_to_address>
The return type annotation for the decorator is too broad and may reduce type safety and clarity.

The return type annotation `Callable[P, R | Any]` is overly broad and may not accurately reflect the actual return type, potentially reducing type safety and maintainability. Consider using a more precise type or a type variable to better capture the decorator's behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

from typing import Self, Union


class ExtendedTimeDelta(timedelta):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Subclassing timedelta may lead to subtle issues due to its C implementation.

Subclassing timedelta can cause unexpected behavior, especially when adding new fields like months and years. Consider using composition instead, or clearly document the limitations and test for edge cases.

Comment on lines +16 to +22
__slots__ = (
'_days',
'_hashcode',
'_microseconds',
'_months',
'_seconds',
'_years',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Defining slots may not be compatible with all uses of timedelta subclassing.

Subclasses of C extension types like timedelta may not safely support slots. Consider removing slots or verify all parent classes are compatible to avoid potential bugs or crashes.

Comment on lines +67 to +68
cls.DAYS_IN_MONTH = days_in_month
cls.DAYS_IN_YEAR = days_in_year

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Assigning DAYS_IN_MONTH and DAYS_IN_YEAR as class variables can cause thread-safety issues.

Since these variables are shared across all instances, concurrent modifications can cause incorrect behavior. Use instance variables or immutable class constants instead.

parent_result = parent_self - other
return ExtendedTimeDelta.from_timedelta(parent_result)

def __mul__(self, other: Union[timedelta, 'ExtendedTimeDelta']) -> Self:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The mul method signature and implementation do not match typical timedelta behavior.

Update the type hint and docstring to indicate that mul should accept a number (int or float), not a timedelta. The implementation should raise a TypeError if 'other' is not numeric. Also, implement rmul for commutative support.

),
)

def __dict__(self) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Overriding dict as a method is non-standard and may break introspection.

dict should remain an attribute; using it as a method can cause compatibility issues. Please rename the method to something like to_dict.

Comment on lines +704 to +718
for index, test in enumerate(tests):
with self.subTest(
f'Testing method which_of_year (subtest {index}) with inputs: {test}',
):
test_input_weekday, test_input_date, test_expected_output = test

output = TemporalAdjuster.which_of_year(
test_input_weekday,
test_input_date,
)

self.assertEqual(
output,
test_expected_output,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Avoid loops in tests. (no-loop-in-tests)

ExplanationAvoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:

  • loops
  • conditionals

Some ways to fix this:

  • Use parametrized tests to get rid of the loop.
  • Move the complex logic into helpers.
  • Move the complex part into pytest fixtures.

Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / Don't Put Logic in Tests

Comment thread docs/conf.py
sys.path.insert(0, os.path.abspath('..'))

project = pyproject['project']['name']
copyright = f'2024-{date.today().year}, {pyproject["project"]["maintainers"][0]["name"]}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Don't assign to builtin variable copyright (avoid-builtin-shadow)


ExplanationPython has a number of builtin variables: functions and constants that
form a part of the language, such as list, getattr, and type
(See https://docs.python.org/3/library/functions.html).
It is valid, in the language, to re-bind such variables:

list = [1, 2, 3]

However, this is considered poor practice.

  • It will confuse other developers.
  • It will confuse syntax highlighters and linters.
  • It means you can no longer use that builtin for its original purpose.

How can you solve this?

Rename the variable something more specific, such as integers.
In a pinch, my_list and similar names are colloquially-recognized
placeholders.

(2, 30)

"""
if isinstance(other, int) or isinstance(other, float):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Merge isinstance calls (merge-isinstance)

Suggested change
if isinstance(other, int) or isinstance(other, float):
if isinstance(other, (int, float)):

Comment on lines +466 to +467
parent_str = super().__str__()
if parent_str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
parent_str = super().__str__()
if parent_str:
if parent_str := super().__str__():

minute = int(seconds // 60)
seconds %= 60

return time(hour, minute, int(seconds), int((seconds - int(seconds)) * 1e6))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): We've found these issues:

Suggested change
return time(hour, minute, int(seconds), int((seconds - int(seconds)) * 1e6))
return time(hour, minute, seconds, int(0 * 1e6))

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@qodo-code-review

qodo-code-review Bot commented Aug 5, 2025

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix incorrect package name placeholder

The package name "pkgname" is a placeholder and should be replaced with the
actual package name "temporal_adjuster". This will ensure the py.typed file is
properly included in the package distribution.

pyproject.toml [46-47]

 [tool.setuptools.package-data]
-"pkgname" = ["py.typed"]
+"temporal_adjuster" = ["py.typed"]
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that pkgname is a placeholder in pyproject.toml, which makes the packaging configuration invalid and non-functional.

High
Add weekday validation check

The method doesn't validate that the given date actually falls on the specified
weekday. This could return incorrect occurrence numbers if the date's weekday
doesn't match the input weekday parameter. Add validation to ensure the date's
weekday matches the normalized weekday parameter.

temporal_adjuster/modules/weekday_operations.py [452-466]

 def which_of_month(weekday: Weekday | ISOWeekday, date: AnyDate) -> int:
 	"""
 	Returns the occurrence of the given day of the week in the month of the given date.
 
 	Args:
 		weekday (Weekday): The day of the week.
 		date (DateT): The reference date.
 
 	Returns:
 		int: The occurrence of the given day of the week in the month of the given date.
 
 	"""
 	weekday = _TemporalAdjusterForWeekday.__normalize_weekday(weekday)
 
+	if date.weekday() != weekday.value:
+		raise ValueError(f'The date {date} does not fall on {weekday.name.lower()}.')
+
 	return (date.day - 1) // 7 + 1
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the which_of_month function ignores the weekday parameter in its calculation, which could lead to incorrect results if the provided date is not on that weekday. Adding the validation check improves the method's correctness and prevents misuse.

Medium
  • Update

@qodo-code-review

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: coverage

Failed stage: Check coverage threshold [❌]

Failed test name: test_extended_timedelta

Failure summary:

The action failed due to multiple test failures and errors:
• Import error: Cannot import
ExtendedTimeDelta from temporal_adjuster.common.types.dates (line 421-422)
• Type error in
test_seconds_to_time_sequence: int() argument must be a string, bytes-like object or real number,
not datetime.time (line 433)
• Assertion failure in test_date_to_int_of_year_sequence: Expected [1,
32, 60] but got [61, 60] (line 441)
• Assertion failure in test_round_time_single: Expected
datetime.time(10, 30) but got datetime.time(10, 15) (line 456)
• Assertion failure in
test_time_difference_sequence: Expected datetime.timedelta objects but got ExtendedTimeDelta objects
(line 463)

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

323:  �[36;1mcoverage report --fail-under=95�[0m
324:  shell: /usr/bin/bash -e {0}
325:  env:
326:  pythonLocation: /opt/hostedtoolcache/Python/3.12.11/x64
327:  PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.12.11/x64/lib/pkgconfig
328:  Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.11/x64
329:  Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.11/x64
330:  Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.11/x64
331:  LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.11/x64/lib
332:  ##[endgroup]
333:  test_date_to_int_of_month_sequence (test_absolute_date_operations.TestAbsoluteDateOperations.test_date_to_int_of_month_sequence)
334:  Test date_to_int_of_month with a sequence of dates. ... ok
335:  test_date_to_int_of_month_single (test_absolute_date_operations.TestAbsoluteDateOperations.test_date_to_int_of_month_single)
336:  Test date_to_int_of_month with a single date. ... ok
337:  test_date_to_int_of_year_sequence (test_absolute_date_operations.TestAbsoluteDateOperations.test_date_to_int_of_year_sequence)
338:  Test date_to_int_of_year with a sequence of dates. ... FAIL
339:  test_date_to_int_of_year_single (test_absolute_date_operations.TestAbsoluteDateOperations.test_date_to_int_of_year_single)
340:  Test date_to_int_of_year with a single date. ... ok
341:  test_edge_cases (test_absolute_date_operations.TestAbsoluteDateOperations.test_edge_cases)
342:  Test edge cases and potential error conditions. ... ok
343:  test_int_to_day_of_month_sequence (test_absolute_date_operations.TestAbsoluteDateOperations.test_int_to_day_of_month_sequence)
344:  Test int_to_day_of_month with a sequence of dates. ... ok
345:  test_int_to_day_of_month_single (test_absolute_date_operations.TestAbsoluteDateOperations.test_int_to_day_of_month_single)
346:  Test int_to_day_of_month with a single date. ... ok
347:  test_int_to_day_of_year_sequence (test_absolute_date_operations.TestAbsoluteDateOperations.test_int_to_day_of_year_sequence)
348:  Test int_to_day_of_year with a sequence of dates. ... ok
349:  test_int_to_day_of_year_single (test_absolute_date_operations.TestAbsoluteDateOperations.test_int_to_day_of_year_single)
350:  Test int_to_day_of_year with a single date. ... ok
351:  test_extended_timedelta (unittest.loader._FailedTest.test_extended_timedelta) ... ERROR
352:  test_first_day_of_last_month_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_first_day_of_last_month_success) ... ok
...

363:  test_last_day_of_last_year_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_last_day_of_last_year_success) ... ok
364:  test_last_day_of_month_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_last_day_of_month_success) ... ok
365:  test_last_day_of_next_month_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_last_day_of_next_month_success) ... ok
366:  test_last_day_of_next_week_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_last_day_of_next_week_success) ... ok
367:  test_last_day_of_next_year_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_last_day_of_next_year_success) ... ok
368:  test_last_day_of_week_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_last_day_of_week_success) ... ok
369:  test_last_day_of_year_success (test_first_and_last_days.TestTemporalAdjusterForFirstAndLastDays.test_last_day_of_year_success) ... ok
370:  test_first_day_of_next_week_success (test_performance.TestPerformance.test_first_day_of_next_week_success) ... ok
371:  test_next_success (test_performance.TestPerformance.test_next_success) ... ok
372:  test_first_day_of_next_week_success (test_sequenceable.TestSequenceable.test_first_day_of_next_week_success) ... ok
373:  test_next_success (test_sequenceable.TestSequenceable.test_next_success) ... ok
374:  test_edge_cases (test_time_operations.TestTimeAdjuster.test_edge_cases) ... ok
375:  test_is_time_in_range_sequence (test_time_operations.TestTimeAdjuster.test_is_time_in_range_sequence) ... ok
376:  test_is_time_in_range_single (test_time_operations.TestTimeAdjuster.test_is_time_in_range_single) ... ok
377:  test_round_time_sequence (test_time_operations.TestTimeAdjuster.test_round_time_sequence) ... ok
378:  test_round_time_single (test_time_operations.TestTimeAdjuster.test_round_time_single) ... FAIL
379:  test_seconds_to_time_sequence (test_time_operations.TestTimeAdjuster.test_seconds_to_time_sequence) ... ERROR
380:  test_seconds_to_time_single (test_time_operations.TestTimeAdjuster.test_seconds_to_time_single) ... ok
381:  test_time_difference_sequence (test_time_operations.TestTimeAdjuster.test_time_difference_sequence) ... FAIL
382:  test_time_difference_single (test_time_operations.TestTimeAdjuster.test_time_difference_single) ... ok
...

396:  test_last_of_year_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_last_of_year_success) ... ok
397:  test_last_or_same_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_last_or_same_success) ... ok
398:  test_last_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_last_success) ... ok
399:  test_next_or_same_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_next_or_same_success) ... ok
400:  test_next_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_next_success) ... ok
401:  test_nth_from_date_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_nth_from_date_success) ... ok
402:  test_nth_of_month_exception_invalid_n (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_nth_of_month_exception_invalid_n) ... ok
403:  test_nth_of_month_exception_no_nth_weekday (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_nth_of_month_exception_no_nth_weekday) ... ok
404:  test_nth_of_month_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_nth_of_month_success) ... ok
405:  test_nth_of_year_exception_invalid_n (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_nth_of_year_exception_invalid_n) ... ok
406:  test_nth_of_year_exception_no_nth_weekday (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_nth_of_year_exception_no_nth_weekday) ... ok
407:  test_nth_of_year_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_nth_of_year_success) ... ok
408:  test_which_of_month_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_which_of_month_success) ... ok
409:  test_which_of_year_success (test_weekday_operations.TestTemporalAdjusterForWeekdays.test_which_of_year_success) ... ok
410:  ======================================================================
411:  ERROR: test_extended_timedelta (unittest.loader._FailedTest.test_extended_timedelta)
412:  ----------------------------------------------------------------------
413:  ImportError: Failed to import test module: test_extended_timedelta
414:  Traceback (most recent call last):
415:  File "/opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/unittest/loader.py", line 396, in _find_test_path
416:  module = self._get_module_from_name(name)
417:  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
418:  File "/opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/unittest/loader.py", line 339, in _get_module_from_name
419:  __import__(name)
420:  File "/home/runner/work/temporal_adjusters_py/temporal_adjusters_py/tests/test_extended_timedelta.py", line 5, in <module>
421:  from temporal_adjuster.common.types.dates import ExtendedTimeDelta
422:  ImportError: cannot import name 'ExtendedTimeDelta' from 'temporal_adjuster.common.types.dates' (/home/runner/work/temporal_adjusters_py/temporal_adjusters_py/temporal_adjuster/common/types/dates.py)
423:  ======================================================================
424:  ERROR: test_seconds_to_time_sequence (test_time_operations.TestTimeAdjuster.test_seconds_to_time_sequence)
425:  ----------------------------------------------------------------------
426:  Traceback (most recent call last):
427:  File "/home/runner/work/temporal_adjusters_py/temporal_adjusters_py/tests/test_time_operations.py", line 222, in test_seconds_to_time_sequence
428:  result = TemporalAdjuster.seconds_to_time(seconds)
429:  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
430:  File "/home/runner/work/temporal_adjusters_py/temporal_adjusters_py/temporal_adjuster/common/decorators/sequence_processor.py", line 45, in wrapper
431:  target_value[index[0]] = result
432:  ~~~~~~~~~~~~^^^^^^^^^^
433:  TypeError: int() argument must be a string, a bytes-like object or a real number, not 'datetime.time'
434:  ======================================================================
435:  FAIL: test_date_to_int_of_year_sequence (test_absolute_date_operations.TestAbsoluteDateOperations.test_date_to_int_of_year_sequence)
436:  Test date_to_int_of_year with a sequence of dates.
437:  ----------------------------------------------------------------------
438:  Traceback (most recent call last):
439:  File "/home/runner/work/temporal_adjusters_py/temporal_adjusters_py/tests/test_absolute_date_operations.py", line 114, in test_date_to_int_of_year_sequence
440:  self.assertEqual(result, expected)
441:  AssertionError: Lists differ: [61, 60] != [1, 32, 60]
442:  First differing element 0:
443:  61
444:  1
445:  Second list contains 1 additional elements.
446:  First extra element 2:
447:  60
448:  - [61, 60]
449:  + [1, 32, 60]
450:  ======================================================================
451:  FAIL: test_round_time_single (test_time_operations.TestTimeAdjuster.test_round_time_single)
452:  ----------------------------------------------------------------------
453:  Traceback (most recent call last):
454:  File "/home/runner/work/temporal_adjusters_py/temporal_adjusters_py/tests/test_time_operations.py", line 126, in test_round_time_single
455:  self.assertEqual(
456:  AssertionError: datetime.time(10, 15) != datetime.time(10, 30)
457:  ======================================================================
458:  FAIL: test_time_difference_sequence (test_time_operations.TestTimeAdjuster.test_time_difference_sequence)
459:  ----------------------------------------------------------------------
460:  Traceback (most recent call last):
461:  File "/home/runner/work/temporal_adjusters_py/temporal_adjusters_py/tests/test_time_operations.py", line 44, in test_time_difference_sequence
462:  self.assertEqual(result, expected)
463:  AssertionError: Lists differ: [ExtendedTimeDelta(seconds=3600), Extended[42 chars]600)] != [datetime.timedelta(seconds=3600), datetim[54 chars]600)]
464:  First differing element 2:
465:  ExtendedTimeDelta(seconds=39600)
466:  datetime.timedelta(days=-1, seconds=39600)
467:  - [ExtendedTimeDelta(seconds=3600),
468:  -  ExtendedTimeDelta(0),
469:  -  ExtendedTimeDelta(seconds=39600)]
470:  + [datetime.timedelta(seconds=3600),
471:  +  datetime.timedelta(0),
472:  +  datetime.timedelta(days=-1, seconds=39600)]
473:  ----------------------------------------------------------------------
474:  Ran 68 tests in 10.312s
475:  FAILED (failures=3, errors=2)
476:  ##[error]Process completed with exit code 1.
477:  Post job cleanup.

…in permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants