From ed03ff154911a2ae6750735cac6310964200af0c Mon Sep 17 00:00:00 2001 From: Alastair Maw <275673+herebebeasties@users.noreply.github.com> Date: Mon, 12 Feb 2024 23:34:00 +0000 Subject: [PATCH 1/6] Tooling --- .github/workflows/main.yml | 10 +++++++--- .pre-commit-config.yaml | 15 ++++++--------- pyproject.toml | 13 ++++++++++--- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c3fc713..bf655635 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,9 +35,13 @@ jobs: python -m pip install --upgrade pip pip install -r etc/${{matrix.requirements_file}} pip install -e . - - name: Lint with flake8 - run: | - flake8 - name: Test with pytest run: | pytest tests -n auto --dist loadscope + + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e1c16f1..c3537204 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.5.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/pycqa/flake8 - rev: 6.1.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.1 hooks: - - id: flake8 -- repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - language_version: python + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index d98121a6..681d2acd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,12 +38,12 @@ dynamic = ["version"] [project.optional-dependencies] dev = [ - "flake8", "hypothesis", "pytest", "pytest-benchmark", "pytest-xdist", "pip-tools", + "ruff", ] [project.scripts] @@ -61,6 +61,13 @@ include = ["exchange_calendars", "exchange_calendars.*"] [tool.setuptools_scm] write_to = "exchange_calendars/_version.py" -[tool.black] +[tool.ruff] +target-version = "py39" line-length = 88 -target-version = ['py39', 'py310', 'py311'] +src = ["etc", "exchange_calendars", "tests"] + +[tool.ruff.lint] +# TODO - decide if we want sorted imports - "I" if so +select = ["E", "F", "B"] +fixable = ["ALL"] +ignore = ["E501", "B019"] From 8fa2ebfac38b609e00a1f9a19a193dad57fee97e Mon Sep 17 00:00:00 2001 From: Alastair Maw <275673+herebebeasties@users.noreply.github.com> Date: Mon, 12 Feb 2024 23:34:54 +0000 Subject: [PATCH 2/6] Fixes --- etc/lunisolar | 15 ++++++------ exchange_calendars/exchange_calendar.py | 8 ++++--- .../pandas_extensions/holiday.py | 2 +- tests/test_calendar_helpers.py | 7 ++++-- tests/test_exchange_calendar.py | 23 +++++++++++-------- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/etc/lunisolar b/etc/lunisolar index 05447eaa..d81e0c7c 100755 --- a/etc/lunisolar +++ b/etc/lunisolar @@ -28,9 +28,8 @@ import novas.compat as novas import numpy as np import pandas as pd import toolz -from novas.compat import eph_manager - from exchange_calendars.calendar_helpers import UTC +from novas.compat import eph_manager @contextlib.contextmanager @@ -878,11 +877,11 @@ def lunar_ecliptic_longitude(root_dir, verbose, start, stop): with ephemeris(): minutes = np.arange(start, stop, dtype="M8[m]") - for minutes in toolz.partition_all(60 * 24 * 365, minutes): + for mins_in_year in toolz.partition_all(60 * 24 * 365, minutes): if verbose: - print(f"start={minutes[0]}; stop={minutes[-1]}") + print(f"start={mins_in_year[0]}; stop={mins_in_year[-1]}") - lunar_ecliptic_longitude_block(root_dir, np.array(minutes)) + lunar_ecliptic_longitude_block(root_dir, np.array(mins_in_year)) def calculate_new_moon(solar_ecliptic_longitude, lunar_ecliptic_longitude, minutes): @@ -1025,11 +1024,11 @@ def solar_solar_ecliptic_longitude(root_dir, verbose, start, stop): with ephemeris(): minutes = np.arange(start, stop, dtype="M8[m]") - for minutes in toolz.partition_all(60 * 24 * 365, minutes): + for mins_in_year in toolz.partition_all(60 * 24 * 365, minutes): if verbose: - print(f"start={minutes[0]}; stop={minutes[-1]}") + print(f"start={mins_in_year[0]}; stop={mins_in_year[-1]}") - solar_ecliptic_longitude_block(root_dir, np.array(minutes)) + solar_ecliptic_longitude_block(root_dir, np.array(mins_in_year)) def minutes_at_phase(longitude, minutes, phase): diff --git a/exchange_calendars/exchange_calendar.py b/exchange_calendars/exchange_calendar.py index e57127ac..7b5b7937 100644 --- a/exchange_calendars/exchange_calendar.py +++ b/exchange_calendars/exchange_calendar.py @@ -66,6 +66,8 @@ WEEKDAYS = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY) WEEKENDS = (SATURDAY, SUNDAY) +ONE_MINUTE = pd.Timedelta(1, "min") + def selection( arr: pd.DatetimeIndex, start: pd.Timestamp, end: pd.Timestamp @@ -119,7 +121,7 @@ def __init__( def __call__(self, f: Callable) -> Callable: @functools.wraps(f) def wrapped_f(*args, **kwargs): - warnings.warn(self._message(f), FutureWarning) + warnings.warn(self._message(f), FutureWarning, stacklevel=2) return f(*args, **kwargs) return wrapped_f @@ -2010,7 +2012,7 @@ def minute_offset_by_sessions( return last_minute elif self.is_break_minute(minute, _parse=False): return self.session_last_am_minute(target_session, _parse=False) - assert False, "offset minute should have resolved!" + raise AssertionError("offset minute should have resolved!") # Methods that evaluate or interrogate a range of minutes. @@ -2344,7 +2346,7 @@ def trading_index( force: bool | None = None, curtail_overlaps: bool = False, ignore_breaks: bool = False, - align: pd.Timedelta | str = pd.Timedelta(1, "min"), + align: pd.Timedelta | str = ONE_MINUTE, align_pm: pd.Timedelta | bool = True, parse: bool = True, ) -> pd.DatetimeIndex | pd.IntervalIndex: diff --git a/exchange_calendars/pandas_extensions/holiday.py b/exchange_calendars/pandas_extensions/holiday.py index 1d2d317d..772e5957 100644 --- a/exchange_calendars/pandas_extensions/holiday.py +++ b/exchange_calendars/pandas_extensions/holiday.py @@ -114,7 +114,7 @@ def _apply_observance(self, dates): offset = observance def f(d): - return d + offset + return d + offset # noqa:B023 observance = f dates = dates.map(observance) diff --git a/tests/test_calendar_helpers.py b/tests/test_calendar_helpers.py index 6d44479f..7f228ef3 100644 --- a/tests/test_calendar_helpers.py +++ b/tests/test_calendar_helpers.py @@ -27,6 +27,9 @@ # TODO tests for next_divider_idx, previous_divider_idx, compute_minutes (#15) +ONE_MINUTE = pd.Timedelta(1, "min") +ONE_DAY = pd.Timedelta(1, "D") + def test_constants(): # Just to make sure they aren't inadvertently changed @@ -502,8 +505,8 @@ def st_start_end( @st.composite def st_periods( draw, - minimum: pd.Timedelta = pd.Timedelta(1, "min"), - maximum: pd.Timedelta = pd.Timedelta(1, "D") - pd.Timedelta(1, "min"), + minimum: pd.Timedelta = ONE_MINUTE, + maximum: pd.Timedelta = ONE_DAY - ONE_MINUTE, ) -> st.SearchStrategy[pd.Timedelta]: """SearchStrategy for a period between a `minimum` and `maximum`.""" period = draw(st.integers(minimum.seconds // 60, maximum.seconds // 60)) diff --git a/tests/test_exchange_calendar.py b/tests/test_exchange_calendar.py index 44848e34..3aeeee89 100644 --- a/tests/test_exchange_calendar.py +++ b/tests/test_exchange_calendar.py @@ -1869,7 +1869,7 @@ def early_closes_sample_time(self) -> abc.Iterator[pd.Timedelta | None]: yield None @pytest.fixture - def early_closes_weekdays(self) -> abc.Iterator[tuple(int)]: + def early_closes_weekdays(self) -> abc.Iterator[tuple[int]]: """Weekdays with non-standard close times. `test_early_closes_weekdays` will check that all sessions on these @@ -2842,9 +2842,7 @@ def test_date_to_session(self, default_calendar_with_answers): # direction as "next" last_session = None - for date, is_session in zip( - dates.sort_values(ascending=False), date_is_session[::-1] - ): + for date, _ in zip(dates.sort_values(ascending=False), date_is_session[::-1]): session_label = f(date, "next") if date in sessions: assert session_label == date @@ -3313,9 +3311,9 @@ def test_minute_to_trading_minute(self, all_calendars_with_answers, all_directio for minutes, session in ans.break_minutes[:1]: for minute in minutes: if direction == "previous": - f(minute, direction) == ans.last_am_minutes[session] + assert f(minute, direction) == ans.last_am_minutes[session] elif direction == "next": - f(minute, direction) == ans.first_pm_minutes[session] + assert f(minute, direction) == ans.first_pm_minutes[session] else: error_msg = ( f"`minute` '{minute}' is not a trading minute. Consider passing" @@ -3887,9 +3885,14 @@ def unite(dtis: list[pd.DatetimeIndex]) -> pd.DatetimeIndex: break def get_index(closed: str, intervals: bool): - start, end = sessions[0], sessions[-1] + start, end = sessions[0], sessions[-1] # noqa: B023 return cal.trading_index( - start, end, period, intervals, closed, parse=False + start, + end, + period, # noqa: B023 + intervals, + closed, + parse=False, # noqa: B023 ) def tst_indices_index( @@ -3911,7 +3914,9 @@ def tst_intervals_index(closed: str, overlaps: bool): if not overlaps: rtrn = get_index(closed, True) expected = pd.IntervalIndex.from_arrays( - left_index, right_index, closed + left_index, # noqa: B023 + right_index, # noqa: B023 + closed, # noqa: B023 ) pd.testing.assert_index_equal(expected, rtrn) else: From bf44e827af2a686eb1f3a4060835050de72d3c82 Mon Sep 17 00:00:00 2001 From: Alastair Maw <275673+herebebeasties@users.noreply.github.com> Date: Mon, 12 Feb 2024 23:35:06 +0000 Subject: [PATCH 3/6] Formatting --- .github/pull_request_template.md | 4 ++-- exchange_calendars/exchange_calendar_xbom.py | 2 +- exchange_calendars/exchange_calendar_xhkg.py | 4 +++- exchange_calendars/exchange_calendar_xidx.py | 3 +-- exchange_calendars/pandas_extensions/offsets.py | 2 -- exchange_calendars/tase_holidays.py | 10 +++++----- exchange_calendars/us_holidays.py | 2 +- exchange_calendars/xkls_holidays.py | 2 +- tests/test_xfra_calendar.py | 4 ++-- tests/test_xhkg_calendar.py | 8 +++++++- tests/test_xkrx_calendar.py | 2 +- tests/test_xnze_calendar.py | 6 ++++-- 12 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a77479ac..beca76d2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ It's recommended that whilst working through the process reference be made to ex - [ ] Name subclass `{Exchange MIC}ExchangeCalendar`. - [ ] Override methods and properties as required, being guided by ExchangeCalendar documentation and in-code comments. All abstract properties must be overriden. - [ ] Include references / links for holidays and timings (either to class documentation or as comments). -- [ ] Import calendar class to `exchange_calendars/calendar_utils.py` and add class to `_default_calendar_factories`. +- [ ] Import calendar class to `exchange_calendars/calendar_utils.py` and add class to `_default_calendar_factories`. - [ ] Add calendar test module `tests/test_{Exchange MIC}_calendar.py` Module should contain a subclass of the test base class `ExchangeCalendarTestBase` (in `tests/test_exchange_calendars.py`). - [ ] Name subclass `Test{Exchange MIC}Calendar`. - [ ] Override fixtures as required, being guided by ExchangeCalendarTestBase documentation and in-code comments. The `calendar_cls` and `max_session_hours` fixtures must be overriden. @@ -16,7 +16,7 @@ It's recommended that whilst working through the process reference be made to ex **Workflow to modify an existing Exchange Calendar** -- [ ] Modify calendar class as required. +- [ ] Modify calendar class as required. - [ ] Modify the test resources file (e.g `tests/resources/{Exchange MIC}.csv`), either manually or by executing `python etc/make_exchange_calendar_test_csv.py {Exchange MIC}`. - [ ] Check if any of the fixtures in `tests/test_{Exchange MIC}_calendar.py` need updating to reflect your changes. - [ ] Add references to any new/modified holidays in `exchange_calendars/exchange_calendar_{Exchange MIC}.py`. diff --git a/exchange_calendars/exchange_calendar_xbom.py b/exchange_calendars/exchange_calendar_xbom.py index d0c6dfad..dea2cece 100644 --- a/exchange_calendars/exchange_calendar_xbom.py +++ b/exchange_calendars/exchange_calendar_xbom.py @@ -431,7 +431,7 @@ "2024-10-02", "2024-11-01", "2024-11-15", - "2024-12-25" + "2024-12-25", ] ) diff --git a/exchange_calendars/exchange_calendar_xhkg.py b/exchange_calendars/exchange_calendar_xhkg.py index 35e54637..874ecbb1 100644 --- a/exchange_calendars/exchange_calendar_xhkg.py +++ b/exchange_calendars/exchange_calendar_xhkg.py @@ -232,7 +232,9 @@ def process_queen_birthday(dt): # on the Monday (2022-09-12). In the past they don't seem to have followed # this pattern. We'll have to wait and see before we generalise this into a rule. pd.Timestamp("2022-09-12"), - pd.Timestamp("2023-07-17"), # 8号台风泰利, 全天休市 https://www.hkex.com.hk/News/Market-Communications/2023/2307172news?sc_lang=en + pd.Timestamp( + "2023-07-17" + ), # 8号台风泰利, 全天休市 https://www.hkex.com.hk/News/Market-Communications/2023/2307172news?sc_lang=en ] diff --git a/exchange_calendars/exchange_calendar_xidx.py b/exchange_calendars/exchange_calendar_xidx.py index f9667a60..783f28be 100644 --- a/exchange_calendars/exchange_calendar_xidx.py +++ b/exchange_calendars/exchange_calendar_xidx.py @@ -101,8 +101,7 @@ def regular_holidays(self): chinese_new_year = chinese_lunar_new_year_dates[ # The Indonesia Stock Exchange did not close for Chinese New # Year in 1998, 1999, or 2001. (It fell on a Saturday in 2000.) - chinese_lunar_new_year_dates.year - >= 2002 + chinese_lunar_new_year_dates.year >= 2002 ] common_leave = pd.to_datetime( diff --git a/exchange_calendars/pandas_extensions/offsets.py b/exchange_calendars/pandas_extensions/offsets.py index ef8626a1..84dabe96 100644 --- a/exchange_calendars/pandas_extensions/offsets.py +++ b/exchange_calendars/pandas_extensions/offsets.py @@ -12,7 +12,6 @@ class CompositeCustomBusinessDay(CustomBusinessDay): - _prefix = "C" _attributes = tuple( [ @@ -245,7 +244,6 @@ def _get_calendar(weekmask, holidays, calendar): class MultipleWeekmaskCustomBusinessDay(CompositeCustomBusinessDay): - _prefix = "C" _attributes = tuple( [ diff --git a/exchange_calendars/tase_holidays.py b/exchange_calendars/tase_holidays.py index 998c726d..3e345906 100644 --- a/exchange_calendars/tase_holidays.py +++ b/exchange_calendars/tase_holidays.py @@ -360,35 +360,35 @@ def holiday(self): month=1, day=1, offset=[_Sukkoth(), Day(1)], - days_of_week=(0, 1, 2, 3, 6) + days_of_week=(0, 1, 2, 3, 6), ) SukkothInterimDay2 = Holiday( "Sukkoth Interim Day", month=1, day=1, offset=[_Sukkoth(), Day(2)], - days_of_week=(0, 1, 2, 3, 6) + days_of_week=(0, 1, 2, 3, 6), ) SukkothInterimDay3 = Holiday( "Sukkoth Interim Day", month=1, day=1, offset=[_Sukkoth(), Day(3)], - days_of_week=(0, 1, 2, 3, 6) + days_of_week=(0, 1, 2, 3, 6), ) SukkothInterimDay4 = Holiday( "Sukkoth Interim Day", month=1, day=1, offset=[_Sukkoth(), Day(4)], - days_of_week=(0, 1, 2, 3, 6) + days_of_week=(0, 1, 2, 3, 6), ) SukkothInterimDay5 = Holiday( "Sukkoth Interim Day", month=1, day=1, offset=[_Sukkoth(), Day(5)], - days_of_week=(0, 1, 2, 3, 6) + days_of_week=(0, 1, 2, 3, 6), ) # Passover interim days are the days between beginning and end of passover. Any otherwise regular business day in that diff --git a/exchange_calendars/us_holidays.py b/exchange_calendars/us_holidays.py index b905b0a4..61f6a79e 100644 --- a/exchange_calendars/us_holidays.py +++ b/exchange_calendars/us_holidays.py @@ -141,7 +141,7 @@ def following_tuesday_every_four_years_observance(dt): month=6, day=19, start_date=Timestamp("2022-01-01"), - observance=nearest_workday + observance=nearest_workday, ) USIndependenceDayBefore1954 = Holiday( "July 4th", diff --git a/exchange_calendars/xkls_holidays.py b/exchange_calendars/xkls_holidays.py index a5108090..6ef09c8b 100644 --- a/exchange_calendars/xkls_holidays.py +++ b/exchange_calendars/xkls_holidays.py @@ -430,7 +430,7 @@ "2021-05-26", "2022-05-15", "2023-05-04", - "2024-05-22" + "2024-05-22", ] ) diff --git a/tests/test_xfra_calendar.py b/tests/test_xfra_calendar.py index 0fad34de..e5bf03bc 100644 --- a/tests/test_xfra_calendar.py +++ b/tests/test_xfra_calendar.py @@ -33,7 +33,7 @@ def regular_holidays_sample(self): # Whit Monday "2015-05-25", # regularly observed from 2015 "2016-05-16", - "2021-05-24" + "2021-05-24", ] @pytest.fixture @@ -58,7 +58,7 @@ def non_holidays_sample(self): "2016-10-31", "2018-10-31", # German Unity Day was not observed in 2022 - "2022-10-03" + "2022-10-03", ] @pytest.fixture diff --git a/tests/test_xhkg_calendar.py b/tests/test_xhkg_calendar.py index d29fc50c..e62a3e2d 100644 --- a/tests/test_xhkg_calendar.py +++ b/tests/test_xhkg_calendar.py @@ -183,7 +183,13 @@ def adhoc_holidays_sample(self): "2022-09-12" # In 2019, the mid-autumn festival holiday was observed on the following business day. ] - yield lunar_2003 + lunar_2018 + lunar_2017 + typhoon_days + mid_autumn_festival_holidays + yield ( + lunar_2003 + + lunar_2018 + + lunar_2017 + + typhoon_days + + mid_autumn_festival_holidays + ) @pytest.fixture def early_closes_sample(self): diff --git a/tests/test_xkrx_calendar.py b/tests/test_xkrx_calendar.py index 14e5896f..80417b6e 100644 --- a/tests/test_xkrx_calendar.py +++ b/tests/test_xkrx_calendar.py @@ -81,7 +81,7 @@ def regular_holidays_sample(self): "2022-10-10", # Buddha's birthday was on 27th May (Saturday), # so the next monday becomes alternative holiday - "2023-05-29" + "2023-05-29", ] @pytest.fixture diff --git a/tests/test_xnze_calendar.py b/tests/test_xnze_calendar.py index fa767ae5..ca065eb2 100644 --- a/tests/test_xnze_calendar.py +++ b/tests/test_xnze_calendar.py @@ -45,8 +45,10 @@ def regular_holidays_sample(self): @pytest.fixture def adhoc_holidays_sample(self): yield [ - "2022-06-24", "2035-06-29", "2049-06-25", # Mataraki Day (a small selection) - "2022-09-26" # National day of mourning for the queen + "2022-06-24", # Mataraki Day (a small selection) + "2035-06-29", # Mataraki Day + "2049-06-25", # Mataraki Day + "2022-09-26", # National day of mourning for the queen ] @pytest.fixture From 27fa9db4fdaa4a9addb051bd8ee92231262136c1 Mon Sep 17 00:00:00 2001 From: Alastair Maw <275673+herebebeasties@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:09:53 +0000 Subject: [PATCH 4/6] Update codestyle badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18e2aa96..b75d7436 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # exchange_calendars -[![PyPI](https://img.shields.io/pypi/v/exchange-calendars)](https://pypi.org/project/exchange-calendars/) ![Python Support](https://img.shields.io/pypi/pyversions/exchange_calendars) ![PyPI Downloads](https://img.shields.io/pypi/dd/exchange-calendars) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![PyPI](https://img.shields.io/pypi/v/exchange-calendars)](https://pypi.org/project/exchange-calendars/) ![Python Support](https://img.shields.io/pypi/pyversions/exchange_calendars) ![PyPI Downloads](https://img.shields.io/pypi/dd/exchange-calendars) [![Code style: black](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) A Python library for defining and querying calendars for security exchanges. From 7a0f637925202b9ea62aa8b54bad4c77474a66cc Mon Sep 17 00:00:00 2001 From: Alastair Maw <275673+herebebeasties@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:36:14 +0100 Subject: [PATCH 5/6] Remove exit non zero on ruff --fix changes --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3537204..b40dcb45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,5 +9,5 @@ repos: rev: v0.2.1 hooks: - id: ruff - args: [--fix, --exit-non-zero-on-fix] + args: [--fix] - id: ruff-format From 631036266b7d392dd4df9248c498c4ab988e23e8 Mon Sep 17 00:00:00 2001 From: Alastair Maw <275673+herebebeasties@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:43:59 +0100 Subject: [PATCH 6/6] Add back in --exit-non-zero-on-fix for CI --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b40dcb45..4c96d170 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,5 +9,7 @@ repos: rev: v0.2.1 hooks: - id: ruff - args: [--fix] + # We have --exit-non-zero-on-fix so that the + # CI build fails if there are any fixes + args: [--fix, --exit-non-zero-on-fix] - id: ruff-format