diff --git a/.cruft.json b/.cruft.json index 4e597c4..b12f029 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/sunpy/package-template", - "commit": "aec53b81aed2e7e534045e59303d82712fe82fb1", + "commit": "75f84c4adf1753af67967930c3335bc73bca9bf5", "checkout": null, "context": { "cookiecutter": { @@ -16,7 +16,8 @@ "enable_dynamic_dev_versions": "y", "include_example_code": "n", "include_cruft_update_github_workflow": "y", - "_sphinx_theme": "alabaster", + "use_extended_ruff_linting": "y", + "_sphinx_theme": "sunpy", "_parent_project": "", "_install_requires": "", "_copy_without_render": [ diff --git a/.github/workflows/label_sync.yml b/.github/workflows/label_sync.yml new file mode 100644 index 0000000..7f21775 --- /dev/null +++ b/.github/workflows/label_sync.yml @@ -0,0 +1,23 @@ +name: Label Sync +on: + workflow_dispatch: + schedule: + # ┌───────── minute (0 - 59) + # │ ┌───────── hour (0 - 23) + # │ │ ┌───────── day of the month (1 - 31) + # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) + - cron: '0 0 * * *' # run every day at midnight UTC + +# Give permissions to write issue labels +permissions: + issues: write + +jobs: + label_sync: + runs-on: ubuntu-latest + name: Label Sync + steps: + - uses: srealmoreno/label-sync-action@850ba5cef2b25e56c6c420c4feed0319294682fd + with: + config-file: https://raw.githubusercontent.com/sunpy/.github/main/labels.yml diff --git a/.github/workflows/sub_package_update.yml b/.github/workflows/sub_package_update.yml index 7455847..0b657f2 100644 --- a/.github/workflows/sub_package_update.yml +++ b/.github/workflows/sub_package_update.yml @@ -21,14 +21,6 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: true - matrix: - include: - - add-paths: . - body: apply the changes to this repo. - branch: cruft/update - commit-message: "Automatic package template update" - title: Updates from the package template - steps: - uses: actions/checkout@v4 @@ -55,25 +47,47 @@ jobs: echo "has_changes=$CHANGES" >> "$GITHUB_OUTPUT" - name: Run update if available + id: cruft_update if: steps.check.outputs.has_changes == '1' run: | git config --global user.email "${{ github.actor }}@users.noreply.github.com" git config --global user.name "${{ github.actor }}" - cruft update --skip-apply-ask --refresh-private-variables + cruft_output=$(cruft update --skip-apply-ask --refresh-private-variables) + echo $cruft_output git restore --staged . - - name: Create pull request + if [[ "$cruft_output" == *"Failed to cleanly apply the update, there may be merge conflicts."* ]]; then + echo merge_conflicts=1 >> $GITHUB_OUTPUT + else + echo merge_conflicts=0 >> $GITHUB_OUTPUT + fi + + - name: Check if only .cruft.json is modified + id: cruft_json if: steps.check.outputs.has_changes == '1' + run: | + git status --porcelain=1 + if [[ "$(git status --porcelain=1)" == " M .cruft.json" ]]; then + echo "Only .cruft.json is modified. Exiting workflow early." + echo "has_changes=0" >> "$GITHUB_OUTPUT" + else + echo "has_changes=1" >> "$GITHUB_OUTPUT" + fi + + - name: Create pull request + if: steps.cruft_json.outputs.has_changes == '1' uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} - add-paths: ${{ matrix.add-paths }} - commit-message: ${{ matrix.commit-message }} - branch: ${{ matrix.branch }} + add-paths: "." + commit-message: "Automatic package template update" + branch: "cruft/update" delete-branch: true - branch-suffix: timestamp - title: ${{ matrix.title }} + draft: ${{ steps.cruft_update.outputs.merge_conflicts == '1' }} + title: "Updates from the package template" body: | - This is an autogenerated PR, which will ${{ matrix.body }}. - [Cruft](https://cruft.github.io/cruft/) has detected updates from the Package Template + This is an autogenerated PR, which will applies the latest changes from the [SunPy Package Template](https://github.com/sunpy/package-template). + If this pull request has been opened as a draft there are conflicts which need fixing. + + **To run the CI on this pull request you will need to close it and reopen it.** diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0b5c91..ec8b80a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.6.9" + rev: "v0.7.2" hooks: - id: ruff args: ['--fix', '--unsafe-fixes'] diff --git a/.ruff.toml b/.ruff.toml index 707968c..8134d7f 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,83 +1,81 @@ target-version = "py310" line-length = 120 exclude = [ - "drms/version.py", ".git,", "__pycache__", - "__pypackages__", - "_build", - ".bzr", - ".direnv", - ".eggs", - ".git-rewrite", - ".git", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "buck-out", "build", - "dist", - "node_modules", - "paper/create_joss_figure.py", - "tools/**", - "venv", + "drms/version.py", ] [lint] select = [ - "E", - "F", - "W", - "UP", - "PT", - "RET", - "TID", - "PLE", - "NPY", - "RUF", - "PGH", - "PTH", - "BLE", - "FBT", - "B", - "A", - "C4", - "T20", - "RSE", - "ERA", + "E", + "F", + "W", + "UP", + "PT", + "BLE", + "A", + "C4", + "INP", + "PIE", + "T20", + "RET", + "TID", + "PTH", + "PD", + "PLC", + "PLE", + "FLY", + "NPY", + "PERF", + "RUF", ] extend-ignore = [ # pycodestyle (E, W) - "E501", # LineTooLong # TODO! fix + "E501", # ignore line length will use a formatter instead + # pyupgrade (UP) + "UP038", # Use | in isinstance - not compatible with models and is slower # pytest (PT) "PT001", # Always use pytest.fixture() "PT004", # Fixtures which don't return anything should have leading _ - "PT007", # Parametrize should be lists of tuples # TODO! fix - "PT011", # Too broad exception assert # TODO! fix "PT023", # Always use () on pytest decorators + # flake8-pie (PIE) + "PIE808", # Disallow passing 0 as the first argument to range + # flake8-use-pathlib (PTH) + "PTH123", # open() should be replaced by Path.open() + # Ruff (RUF) + "RUF003", # Ignore ambiguous quote marks, doesn't allow ' in comments + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "RUF013", # PEP 484 prohibits implicit `Optional` + "RUF015", # Prefer `next(iter(...))` over single element slice ] [lint.per-file-ignores] -# Part of configuration, not a package. -"setup.py" = ["INP001"] -"conftest.py" = ["INP001"] +"setup.py" = [ + "INP001", # File is part of an implicit namespace package. +] +"conftest.py" = [ + "INP001", # File is part of an implicit namespace package. +] "docs/conf.py" = [ - "E402" # Module imports not at top of file + "E402" # Module imports not at top of file ] "docs/*.py" = [ - "INP001", # Implicit-namespace-package. The examples are not a package. + "INP001", # File is part of an implicit namespace package. +] +"examples/**.py" = [ + "T201", # allow use of print in examples + "INP001", # File is part of an implicit namespace package. +] +"__init__.py" = [ + "E402", # Module level import not at top of cell + "F401", # Unused import + "F403", # from {name} import * used; unable to detect undefined names + "F405", # {name} may be undefined, or defined from star imports ] -"__init__.py" = ["E402", "F401", "F403"] -"test_*.py" = ["B011", "D", "E402", "PGH001", "S101"] -"examples/*.py" = [ - "T201", - # We need print in our examples +"test_*.py" = [ + "E402", # Module level import not at top of cell ] "examples/plot_hmi_modes.py" = [ "E741", # Ambiguous variable name diff --git a/docs/conf.py b/docs/conf.py index e09f0a9..7a84090 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ ] # Add any paths that contain templates here, relative to this directory. -# templates_path = ["_templates"] # NOQA: ERA001 +# templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -116,7 +116,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ["_static"] # NOQA: ERA001 +# html_static_path = ["_static"] # By default, when rendering docstrings for classes, sphinx.ext.autodoc will # make docs with the class-level docstring and the class-method docstrings, diff --git a/drms/_dev/scm_version.py b/drms/_dev/scm_version.py index bae5523..d0c4c1f 100644 --- a/drms/_dev/scm_version.py +++ b/drms/_dev/scm_version.py @@ -5,8 +5,8 @@ try: from setuptools_scm import get_version - version = get_version(root=Path("..") / "..", relative_to=__file__) -except ImportError as e: - raise ImportError("setuptools_scm not installed") from e + version = get_version(root=Path("../.."), relative_to=__file__) +except ImportError: + raise except Exception as e: - raise ValueError(f"setuptools_scm broken with {e}") from e + raise ValueError("setuptools_scm can not determine version.") from e diff --git a/drms/client.py b/drms/client.py index 491fe77..d19a353 100644 --- a/drms/client.py +++ b/drms/client.py @@ -67,12 +67,7 @@ def __init__(self, d, *, name=None): @staticmethod def _parse_keywords(d): keys = ["name", "type", "recscope", "defval", "units", "note", "linkinfo"] - res = [] - for di in d: - resi = [] - for k in keys: - resi.append(di.get(k)) - res.append(tuple(resi)) + res = [tuple(di.get(k) for k in keys) for di in d] if not res: res = None # workaround for older pandas versions res = pd.DataFrame(res, columns=keys) @@ -89,12 +84,7 @@ def _parse_keywords(d): @staticmethod def _parse_links(d): keys = ["name", "target", "kind", "note"] - res = [] - for di in d: - resi = [] - for k in keys: - resi.append(di.get(k)) - res.append(tuple(resi)) + res = [tuple(di.get(k) for k in keys) for di in d] if not res: res = None # workaround for older pandas versions res = pd.DataFrame(res, columns=keys) @@ -104,12 +94,7 @@ def _parse_links(d): @staticmethod def _parse_segments(d): keys = ["name", "type", "units", "protocol", "dims", "note"] - res = [] - for di in d: - resi = [] - for k in keys: - resi.append(di.get(k)) - res.append(tuple(resi)) + res = [tuple(di.get(k) for k in keys) for di in d] if not res: res = None # workaround for older pandas versions res = pd.DataFrame(res, columns=keys) @@ -204,7 +189,7 @@ def _generate_download_urls(self): # data_dir contains a directory, the filename column should contain # only the basename and we need to join it with the directory. if data_dir is None: - res.rename(columns={"filename": "fpath"}, inplace=True) + res = res.rename(columns={"filename": "fpath"}) split_fpath = res.fpath.str.split("/") res["filename"] = [sfp[-1] for sfp in split_fpath] else: diff --git a/drms/tests/test_jsoc_export.py b/drms/tests/test_jsoc_export.py index 2ca716c..e2a645a 100644 --- a/drms/tests/test_jsoc_export.py +++ b/drms/tests/test_jsoc_export.py @@ -23,13 +23,13 @@ def test_export_asis_basic(jsoc_client_export, method): for record in r.urls.record: record = record.lower() assert record.startswith("hmi.v_avg120[2150]") - assert record.endswith("{mean}") or record.endswith("{power}") + assert record.endswith(("{mean}", "{power}")) for filename in r.urls.filename: - assert filename.endswith("mean.fits") or filename.endswith("power.fits") + assert filename.endswith(("mean.fits", "power.fits")) for url in r.urls.url: - assert url.endswith("mean.fits") or url.endswith("power.fits") + assert url.endswith(("mean.fits", "power.fits")) @pytest.mark.jsoc() @@ -54,10 +54,10 @@ def test_export_fits_basic(jsoc_client_export): assert record.endswith("2014.11.30_00:00:00_tai]") for filename in r.urls.filename: - assert filename.endswith("continuum.fits") or filename.endswith("magnetogram.fits") + assert filename.endswith(("continuum.fits", "magnetogram.fits")) for url in r.urls.url: - assert url.endswith("continuum.fits") or url.endswith("magnetogram.fits") + assert url.endswith(("continuum.fits", "magnetogram.fits")) @pytest.mark.jsoc() diff --git a/drms/tests/test_to_datetime.py b/drms/tests/test_to_datetime.py index a8bc84c..5deea8f 100644 --- a/drms/tests/test_to_datetime.py +++ b/drms/tests/test_to_datetime.py @@ -92,7 +92,7 @@ def test_time_series(time_series, expected): @pytest.mark.parametrize(("time_string", "expected"), data_invalid) def test_corner_case(time_string, expected): - assert pd.isnull(drms.to_datetime(time_string)) == expected + assert pd.isna(drms.to_datetime(time_string)) == expected assert isinstance(drms.to_datetime([]), pd.Series) assert drms.to_datetime([]).empty @@ -107,4 +107,4 @@ def test_corner_case(time_string, expected): ], ) def test_corner_case_series(time_series, expected): - assert pd.isnull(drms.to_datetime(time_series)).equals(expected) + assert pd.isna(drms.to_datetime(time_series)).equals(expected) diff --git a/pyproject.toml b/pyproject.toml index d69589e..b1c740e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,18 +32,17 @@ tests = [ "astropy", ] docs = [ - "astropy", - "matplotlib", "sphinx", "sphinx-automodapi", + "sunpy-sphinx-theme", + "packaging", + "astropy", + "matplotlib", "sphinx-changelog", "sphinx-copybutton", "sphinx-gallery", "sphinx-hoverxref", "sphinxext-opengraph", - "sunpy-sphinx-theme", - "sphinx-automodapi", - "packaging", ] [project.urls] repository = "https://sunpy.org"