diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..b144fe53 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# Keep dependencies updated with Dependabot version updates +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: ".github/workflows/" + schedule: + interval: "weekly" + groups: + actions: + patterns: + - "*" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 35645244..d1863ec4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -61,7 +61,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -74,6 +74,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 781ca1a6..5da2e1de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,13 +53,25 @@ repos: - id: python-check-blanket-noqa # Enforce that all noqa annotations always occur with specific codes. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.4.7" + hooks: + - id: ruff + # args: ["--fix", "--show-fixes"] + args: ["--show-fixes"] + - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 hooks: - id: pyupgrade - args: ["--py39-plus"] + args: ["--py310-plus"] exclude: ".*(extern.*)$" + - repo: https://github.com/scientific-python/cookie + rev: 2024.04.23 + hooks: + - id: sp-repo-review + - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: @@ -90,6 +102,13 @@ repos: additional_dependencies: - tomli + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.5 + hooks: + - id: docformatter + additional_dependencies: [tomli] + args: [--in-place, --config, ./pyproject.toml] + # - repo: https://github.com/MarcoGorelli/absolufy-imports # rev: v0.3.1 # hooks: diff --git a/dev/regions_parse.py b/dev/regions_parse.py index 1c160766..4fbcc7e4 100644 --- a/dev/regions_parse.py +++ b/dev/regions_parse.py @@ -15,7 +15,9 @@ @click.group() def cli(): - """astropy.regions parser debugging tool.""" + """ + astropy.regions parser debugging tool. + """ pass diff --git a/dev/regions_pyregion_comparison.py b/dev/regions_pyregion_comparison.py index d1307241..a5f2bb08 100644 --- a/dev/regions_pyregion_comparison.py +++ b/dev/regions_pyregion_comparison.py @@ -1,6 +1,6 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst """ -Compare DS9 parsing of the astropy regions package to pyregion +Compare DS9 parsing of the astropy regions package to pyregion. This scripts compares the DS9 parsing of the astropy regions package to pyregion in two regards. diff --git a/docs/conf.py b/docs/conf.py index 47b5fa3c..2d3e9952 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -181,12 +181,13 @@ # Uncomment the following lines to enable the exceptions: nitpick_filename = 'nitpick-exceptions.txt' if os.path.isfile(nitpick_filename): - for line in open(nitpick_filename): - if line.strip() == '' or line.startswith('#'): - continue - dtype, target = line.split(None, 1) - target = target.strip() - nitpick_ignore.append((dtype, target)) + with open(nitpick_filename) as fh: + for line in fh: + if line.strip() == '' or line.startswith('#'): + continue + dtype, target = line.split(None, 1) + target = target.strip() + nitpick_ignore.append((dtype, target)) # -- Options for linkcheck output --------------------------------------------- linkcheck_retry = 5 diff --git a/pyproject.toml b/pyproject.toml index 8828bfc2..5e4b0509 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,14 @@ norecursedirs = [ astropy_header = true doctest_plus = 'enabled' text_file_format = 'rst' -addopts = '--color=yes --doctest-rst --arraydiff' +addopts = [ + '-ra', + '--color=yes', + '--doctest-rst', + '--strict-config', + '--strict-markers', +] +log_cli_level = 'INFO' xfail_strict = true remote_data_strict = true filterwarnings = [ @@ -151,6 +158,17 @@ exclude_lines = [ 'def _ipython_key_completions_', ] +[tool.repo-review] +ignore = [ + 'MY', # ignore MyPy + 'PC110', # ignore using black or ruff-format in pre-commit + 'PC111', # ignore using blacken-docs in pre-commit + 'PC140', # ignore using mypy in pre-commit + 'PC180', # ignore using prettier in pre-commit + 'PC901', # ignore using custom pre-commit update message + 'PY005', # ignore having a tests/ folder +] + [tool.isort] skip_glob = [ 'regions/*__init__.py*', @@ -173,3 +191,43 @@ exclude_dirs = ['*/tests/test_casa_mask.py'] [tool.bandit.assert_used] skips = ['*_test.py', '*/test_*.py', '*/tests/helpers.py'] + +[tool.docformatter] + wrap-summaries = 72 + pre-summary-newline = true + make-summary-multi-line = true + +[tool.ruff.lint] +select = ['E', 'F', 'UP', 'B', 'SIM', 'PL', 'FLY', 'NPY', 'PERF', 'INT', + 'RSE', 'Q', 'N', 'W', 'D', 'I'] +ignore = [ + 'E501', + 'B028', + 'B905', # revisit + 'D100', + 'D101', + 'D102', + 'D103', + 'D105', + 'D107', + 'D200', + 'D205', + 'D212', + 'D404', + 'I001', + 'PLR0912', + 'PLR0913', + 'PLR0915', + 'PLR2004', + 'PLW2901', + 'Q000', + 'SIM910', + 'UP038', +] + +[tool.ruff.lint.per-file-ignores] +'__init__.py' = ['D104', 'I'] +'docs/conf.py' = ['ERA001', 'INP001', 'TRY400'] + +[tool.ruff.lint.pydocstyle] +convention = 'numpy' diff --git a/regions/_geometry/tests/test_circular_overlap_grid.py b/regions/_geometry/tests/test_circular_overlap_grid.py index 9f09190d..94349bd4 100644 --- a/regions/_geometry/tests/test_circular_overlap_grid.py +++ b/regions/_geometry/tests/test_circular_overlap_grid.py @@ -25,7 +25,6 @@ def test_circular_overlap_grid(grid_size, circ_size, use_exact, subsample): Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ - g = circular_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, circ_size, use_exact, subsample) assert_allclose(g.max(), 1.0) diff --git a/regions/_geometry/tests/test_elliptical_overlap_grid.py b/regions/_geometry/tests/test_elliptical_overlap_grid.py index 0b060fd3..54062757 100644 --- a/regions/_geometry/tests/test_elliptical_overlap_grid.py +++ b/regions/_geometry/tests/test_elliptical_overlap_grid.py @@ -31,7 +31,6 @@ def test_elliptical_overlap_grid(grid_size, maj_size, min_size, angle, Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ - g = elliptical_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, maj_size, min_size, angle, use_exact, subsample) diff --git a/regions/_geometry/tests/test_rectangular_overlap_grid.py b/regions/_geometry/tests/test_rectangular_overlap_grid.py index 05dd8a72..0ae3b4a2 100644 --- a/regions/_geometry/tests/test_rectangular_overlap_grid.py +++ b/regions/_geometry/tests/test_rectangular_overlap_grid.py @@ -25,7 +25,6 @@ def test_rectangular_overlap_grid(grid_size, rect_size, angle, subsample): Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ - g = rectangular_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, rect_size, rect_size, angle, 0, subsample) assert_allclose(g.max(), 1.0) diff --git a/regions/_utils/examples.py b/regions/_utils/examples.py index 25a86e15..91d35ebc 100644 --- a/regions/_utils/examples.py +++ b/regions/_utils/examples.py @@ -88,7 +88,9 @@ def __init__(self, config=None): @lazyproperty def wcs(self): - """World coordinate system (`~astropy.wcs.WCS`).""" + """ + World coordinate system (`~astropy.wcs.WCS`). + """ wcs = WCS(naxis=2) wcs.wcs.crval = self.config['crval'] wcs.wcs.crpix = self.config['crpix'] @@ -99,7 +101,9 @@ def wcs(self): @lazyproperty def image(self): - """Counts image (`~astropy.io.fits.ImageHDU`).""" + """ + Return a "counts" image (`~astropy.io.fits.ImageHDU`). + """ events = self.event_table skycoord = SkyCoord(events['GLON'], events['GLAT'], unit='deg', frame='galactic') diff --git a/regions/core/attributes.py b/regions/core/attributes.py index 55886ca4..f2095f48 100644 --- a/regions/core/attributes.py +++ b/regions/core/attributes.py @@ -56,7 +56,8 @@ def _validate(self, value): class ScalarPixCoord(RegionAttribute): """ - Descriptor class to check that value is a scalar `~regions.PixCoord`. + Descriptor class to check that value is a scalar + `~regions.PixCoord`. """ def _validate(self, value): @@ -133,8 +134,8 @@ def _validate(self, value): class PositiveScalarAngle(RegionAttribute): """ - Descriptor class to check that value is a strictly positive - scalar angle, either an `~astropy.coordinates.Angle` or + Descriptor class to check that value is a strictly positive scalar + angle, either an `~astropy.coordinates.Angle` or `~astropy.units.Quantity` with angular units. """ diff --git a/regions/core/bounding_box.py b/regions/core/bounding_box.py index 7cfebf8d..e4a6398c 100644 --- a/regions/core/bounding_box.py +++ b/regions/core/bounding_box.py @@ -209,9 +209,8 @@ def get_overlap_slices(self, shape): def extent(self): """ The extent of the mask, defined as the ``(xmin, xmax, ymin, - ymax)`` bounding box from the bottom-left corner of the - lower-left pixel to the upper-right corner of the upper-right - pixel. + ymax)`` bounding box from the bottom-left corner of the lower- + left pixel to the upper-right corner of the upper-right pixel. The upper edges here are the actual pixel positions of the edges, i.e., they are not "exclusive" indices used for python @@ -261,8 +260,8 @@ def as_artist(self, **kwargs): def to_region(self): """ - Return a `~regions.RectanglePixelRegion` that - represents the bounding box. + Return a `~regions.RectanglePixelRegion` that represents the + bounding box. """ from regions.core.pixcoord import PixCoord from regions.shapes import RectanglePixelRegion diff --git a/regions/core/core.py b/regions/core/core.py index 58df3bf5..c1cef9d3 100644 --- a/regions/core/core.py +++ b/regions/core/core.py @@ -345,10 +345,10 @@ def _validate_mode(mode, subpixels): raise ValueError(f'Invalid mask mode: {mode} (should be one ' f'of {valid_modes}') - if mode == 'subpixels': - if not isinstance(subpixels, int) or subpixels <= 0: - raise ValueError(f'Invalid subpixels value: {subpixels} ' - '(should be a strictly positive integer)') + if (mode == 'subpixels' + and (not isinstance(subpixels, int) or subpixels <= 0)): + raise ValueError(f'Invalid subpixels value: {subpixels} ' + '(should be a strictly positive integer)') @abc.abstractmethod def as_artist(self, origin=(0, 0), **kwargs): diff --git a/regions/core/mask.py b/regions/core/mask.py index a7531d9e..ece64b92 100644 --- a/regions/core/mask.py +++ b/regions/core/mask.py @@ -59,8 +59,8 @@ def shape(self): def get_overlap_slices(self, shape): """ - Get slices for the overlapping part of the region mask and a - 2D array. + Get slices for the overlapping part of the region mask and a 2D + array. Parameters ---------- @@ -175,10 +175,7 @@ def cutout(self, data, fill_value=0.0, copy=False): return cutout # cutout is always a copy for partial overlap - if ~np.isfinite(fill_value): - dtype = float - else: - dtype = data.dtype + dtype = float if ~np.isfinite(fill_value) else data.dtype cutout = np.zeros(self.shape, dtype=dtype) cutout[:] = fill_value cutout[slices_small] = data[slices_large] @@ -267,9 +264,8 @@ def _get_overlap_cutouts(self, shape, mask=None): multiple associated arrays (e.g., data and error arrays). It is used in this way by the `PixelAperture.do_photometry` method. """ - if mask is not None: - if mask.shape != shape: - raise ValueError('mask and data must have the same shape') + if mask is not None and mask.shape != shape: + raise ValueError('mask and data must have the same shape') slc_large, slc_small = self.get_overlap_slices(shape) if slc_large is None: # no overlap diff --git a/regions/core/pixcoord.py b/regions/core/pixcoord.py index 8d938666..032378a0 100644 --- a/regions/core/pixcoord.py +++ b/regions/core/pixcoord.py @@ -104,7 +104,7 @@ def __len__(self): return len(self.x) def __iter__(self): - for (x, y) in zip(self.x, self.y): + for (x, y) in zip(self.x, self.y, strict=True): yield PixCoord(x=x, y=y) def __getitem__(self, key): @@ -129,8 +129,8 @@ def __sub__(self, other): def __eq__(self, other): """ - Checks whether ``other`` is `PixCoord` object and whether - their abscissa and ordinate values are equal using + Check whether ``other`` is `PixCoord` object and whether their + abscissa and ordinate values are equal using `np.testing.assert_allclose` with its default tolerance values. """ if isinstance(other, self.__class__): diff --git a/regions/core/regions.py b/regions/core/regions.py index 17e563bc..20ee93b3 100644 --- a/regions/core/regions.py +++ b/regions/core/regions.py @@ -67,8 +67,8 @@ def append(self, region): def extend(self, regions): """ - Extend the list of regions by appending elements from the - input regions. + Extend the list of regions by appending elements from the input + regions. Parameters ---------- diff --git a/regions/core/registry.py b/regions/core/registry.py index 99888f4f..faf15fb9 100644 --- a/regions/core/registry.py +++ b/regions/core/registry.py @@ -9,13 +9,15 @@ class IORegistryError(Exception): - """Exception class for various registry errors.""" + """ + Exception class for various registry errors. + """ class RegionsRegistry: """ - Class to hold a registry to read, write, parse, and serialize regions - in various formats. + Class to hold a registry to read, write, parse, and serialize + regions in various formats. """ registry = {} @@ -157,7 +159,7 @@ def get_formats(cls, classobj): if len(rows) == 1: return Table() - cols = list(zip(*rows)) + cols = list(zip(*rows, strict=True)) tbl = Table() for col in cols: tbl[col[0]] = col[1:] diff --git a/regions/core/tests/test_compound.py b/regions/core/tests/test_compound.py index be83ea54..cbacba59 100644 --- a/regions/core/tests/test_compound.py +++ b/regions/core/tests/test_compound.py @@ -20,6 +20,7 @@ class TestCompoundPixel: """ Test compound pixel regions. """ + # Two circles that overlap in one column c1 = CirclePixelRegion(PixCoord(5, 5), 4) c2 = CirclePixelRegion(PixCoord(11, 5), 4) diff --git a/regions/core/tests/test_pixcoord.py b/regions/core/tests/test_pixcoord.py index 096a6cf4..bdae9581 100644 --- a/regions/core/tests/test_pixcoord.py +++ b/regions/core/tests/test_pixcoord.py @@ -206,7 +206,7 @@ def test_equality(): pc1 = PixCoord(arr[0], arr[1]) pc2 = PixCoord(arr[0] + 0.0000001, arr[1]) - assert not pc1 == arr + assert pc1 != arr assert pc1 == PixCoord(arr[0], arr[1]) assert pc1 == pc2 diff --git a/regions/io/crtf/io_core.py b/regions/io/crtf/io_core.py index 71fc6cd4..a016d1fd 100644 --- a/regions/io/crtf/io_core.py +++ b/regions/io/crtf/io_core.py @@ -189,7 +189,7 @@ def to_crtf(self, coordsys='fk5', fmt='.6f', radunit='deg'): # cannot recognize a region without an inline coordinate # specification. It can be, but does not need to be, # comma-separated at the start. - shape_coordsys = getattr(shape, 'coordsys') + shape_coordsys = shape.coordsys if shape_coordsys.lower() != coordsys.lower(): coord = coordsys_mapping['CRTF'][coordsys.lower()] if meta_str.strip(): @@ -212,13 +212,11 @@ def to_crtf(self, coordsys='fk5', fmt='.6f', radunit='deg'): coord = [] if coordsys not in ['image', 'physical']: for val in shape.coord: - if isinstance(val, Angle): + if (isinstance(val, Angle) + or (radunit == '' or radunit is None)): coord.append(float(val.value)) else: - if radunit == '' or radunit is None: - coord.append(float(val.value)) - else: - coord.append(float(val.to(radunit).value)) + coord.append(float(val.to(radunit).value)) else: for val in shape.coord: if isinstance(val, u.Quantity): @@ -232,7 +230,7 @@ def to_crtf(self, coordsys='fk5', fmt='.6f', radunit='deg'): if shape.region_type == 'polygon': vals = [f'[{x:{fmt}}deg, {y:{fmt}}deg]' - for x, y in zip(coord[::2], coord[1::2])] + for x, y in zip(coord[::2], coord[1::2], strict=True)] coord = ', '.join(vals) line = crtf_strings['polygon'].format(include, coord) @@ -383,7 +381,7 @@ def _convert_sky_coords(self): Convert to sky coordinates. """ parsed_angles = [] - for x, y in zip(self.coord[:-1:2], self.coord[1::2]): + for x, y in zip(self.coord[:-1:2], self.coord[1::2], strict=True): if isinstance(x, Angle) and isinstance(y, Angle): parsed_angles.append((x, y)) @@ -392,7 +390,7 @@ def _convert_sky_coords(self): if len(parsed_angles) == 0: raise ValueError('error parsing region') - lon, lat = zip(*parsed_angles) + lon, lat = zip(*parsed_angles, strict=True) if (hasattr(lon, '__len__') and hasattr(lat, '__len__') and len(lon) == 1 and len(lat) == 1): # force entries to be scalar if they are length-1 @@ -533,10 +531,8 @@ def _to_shape_list(region_list, coordinate_system='fk5'): if coordinate_system: coordsys = coordinate_system else: - if isinstance(region, SkyRegion): - coordsys = coord[0].name - else: - coordsys = 'image' + coordsys = (coord[0].name + if isinstance(region, SkyRegion) else 'image') new_coord = [] for val in coord: diff --git a/regions/io/crtf/read.py b/regions/io/crtf/read.py index 9e95f80e..caa07398 100644 --- a/regions/io/crtf/read.py +++ b/regions/io/crtf/read.py @@ -274,7 +274,8 @@ class _CRTFRegionParser: # Maps CASA coordinate frame to appropriate astropy coordinate frames. coordsys_mapping = dict(zip(frame_transform_graph.get_names(), - frame_transform_graph.get_names())) + frame_transform_graph.get_names(), + strict=True)) coordsys_mapping['j2000'] = 'fk5' coordsys_mapping['b1950'] = 'fk4' coordsys_mapping['supergal'] = 'supergalactic' @@ -361,6 +362,7 @@ def convert_coordinates(self): 'parameters for the region ' f'"{self.region_type}"') + # TODO: check zip strict=True for attr_spec, val_str in zip(self.language_spec[self.region_type], coord_list_str): if attr_spec == 'c': diff --git a/regions/io/crtf/tests/test_crtf.py b/regions/io/crtf/tests/test_crtf.py index 61c85171..b9844221 100644 --- a/regions/io/crtf/tests/test_crtf.py +++ b/regions/io/crtf/tests/test_crtf.py @@ -45,7 +45,7 @@ def test_valid_crtf_line(): def test_valid_region_type(): """ - Checks whether the region type is valid in CRTF format + Checks whether the region type is valid in CRTF format. """ reg_str = 'hyperbola[[18h12m24s, -23d11m00s], 2.3arcsec]' @@ -79,7 +79,7 @@ def test_valid_meta_key(): def test_valid_region_syntax(): """ - Checks whether the region has valid parameters + Checks whether the region has valid parameters. """ reg_str1 = 'circle[[18h12m24s, -23d11m00s], [2.3arcsec,4.5arcsec]' with pytest.raises(CRTFRegionParserError) as excinfo: @@ -140,8 +140,8 @@ def test_issue_312_regression(): 'fk5', '.6f')]) def test_file_crtf(filename, outname, coordsys, fmt): """ - The "labelcolor" example is a regression test for Issue 405 - The others are just a general serialization self-consistency check. + The "labelcolor" example is a regression test for Issue 405 The + others are just a general serialization self-consistency check. """ filename = get_pkg_data_filename(filename) regs = Regions.read(filename, errors='warn', format='crtf') @@ -180,7 +180,7 @@ def test_crtf_header(): def test_space_after_regname(): """ - Regression test for #271: space is allowed + Regression test for #271: space is allowed. """ reg_str = 'circle [[42deg, 43deg], 3deg], coord=J2000, color=green' reg = Regions.parse(reg_str, format='crtf')[0] diff --git a/regions/io/ds9/read.py b/regions/io/ds9/read.py index 14536525..696f924e 100644 --- a/regions/io/ds9/read.py +++ b/regions/io/ds9/read.py @@ -105,11 +105,8 @@ def _split_lines(region_str): lines : list of str A list of strings. """ - lines = [] - for line in region_str.split('\n'): - for line_ in _split_semicolon(line): - lines.append(line_.strip()) - return lines + return [line_.strip() for line in region_str.split('\n') + for line_ in _split_semicolon(line)] def _parse_raw_data(region_str): @@ -216,11 +213,8 @@ def _parse_raw_data(region_str): composite_meta.pop('composite', None) # NOTE: include=1/0 in metadata overrides the leading - # "-/+" symbol - if include_symbol == '-': - include = 0 - else: # '+' or '' - include = 1 + # "-/+" symbol; -: include=0;, + or '': include=1 + include = 0 if include_symbol == '-' else 1 include_meta = {'include': include} params_str, meta_str = _parse_shape_line(shape, original_line, @@ -332,12 +326,11 @@ def _parse_metadata(metadata_str): if key == 'tag': val = [val] # tag value is always a list metadata[key] = val + elif key == 'tag': + metadata[key].append(val) else: - if key == 'tag': - metadata[key].append(val) - else: - warnings.warn(f'Found duplicate metadata for "{key}", ' - 'skipping', AstropyUserWarning) + warnings.warn(f'Found duplicate metadata for "{key}", ' + 'skipping', AstropyUserWarning) return metadata @@ -403,9 +396,8 @@ def _define_raw_metadata(global_meta, composite_meta, include_meta, val = value.split() if val[0] not in valid_points: is_invalid = True - if len(val) == 2: - if not float(val[1]).is_integer(): - is_invalid = True + if len(val) == 2 and not float(val[1]).is_integer(): + is_invalid = True if key == 'line' and value not in valid_lines: is_invalid = True @@ -549,6 +541,7 @@ def _parse_shape_params(region_data): shape_template = ds9_params_template[shape] shape_params = [] + # TODO: check zip strict=True for idx, (param_type, value) in enumerate(zip(shape_template, params)): if shape in ('ellipse', 'box') and idx == nparams - 1: param_type = 'angle' # last parameter is always an angle @@ -629,7 +622,12 @@ def _define_region_params(region_type, shape, shape_params, frame=None): def _make_region(region_data): """ + Make a region object from the region data. + + Parameters + ---------- region_data : `_RegionData` instance + A `_RegionData` instance. """ try: # NOTE: returned shape can be different from region_data.shape @@ -693,7 +691,7 @@ def _find_text_delim_idx(region_str): start_idx.append(match.span()[1]) idx1 = [] - for sidx, char in zip(start_idx, delim): + for sidx, char in zip(start_idx, delim, strict=True): idx1.append(region_str.find(char, sidx)) return idx0, idx1 @@ -732,7 +730,7 @@ def _split_semicolon(region_str): semi_idx = [pos for pos, char in enumerate(region_str) if char == ';'] fidx = [] for i in semi_idx: - for i0, i1 in zip(idx0, idx1): + for i0, i1 in zip(idx0, idx1, strict=True): if i0 <= i <= i1: break else: @@ -740,4 +738,4 @@ def _split_semicolon(region_str): fidx.insert(0, 0) return [region_str[i:j].rstrip(';') - for i, j in zip(fidx, fidx[1:] + [None])] + for i, j in zip(fidx, fidx[1:] + [None], strict=True)] diff --git a/regions/io/ds9/tests/test_ds9.py b/regions/io/ds9/tests/test_ds9.py index a9a1ebae..0f9d4a4e 100644 --- a/regions/io/ds9/tests/test_ds9.py +++ b/regions/io/ds9/tests/test_ds9.py @@ -37,7 +37,7 @@ def test_roundtrip(tmpdir): regions.write(tempfile, format='ds9', overwrite=True, precision=20) regions2 = Regions.read(tempfile, format='ds9') assert len(regions2) > 0 - for reg1, reg2 in zip(regions, regions2): + for reg1, reg2 in zip(regions, regions2, strict=True): assert_region_allclose(reg1, reg2) @@ -595,8 +595,8 @@ def test_mixed_coord(): def test_unsupported_marker(): """ - Test that warning is issued when serializing a valid matplotlib marker, - but unsupported by DS9. + Test that warning is issued when serializing a valid matplotlib + marker, but unsupported by DS9. """ region = PointPixelRegion(PixCoord(2, 2), visual=RegionVisual(marker='Z')) with pytest.warns(AstropyUserWarning): diff --git a/regions/io/ds9/write.py b/regions/io/ds9/write.py index 7fbd9c4f..e087d4e6 100644 --- a/regions/io/ds9/write.py +++ b/regions/io/ds9/write.py @@ -70,7 +70,7 @@ def _serialize_ds9(regions, precision=8): output += f'{global_frame}\n' # add line for each region - for region, region_meta in zip(region_data, metadata): + for region, region_meta in zip(region_data, metadata, strict=True): if global_frame is None: output += f'{region["frame"]}; ' @@ -123,17 +123,16 @@ def _get_region_shape(region): def _get_frame_name(region, mapping): if isinstance(region, PixelRegion): frame = 'image' + elif 'center' in region._params: + frame = region.center.frame.name + elif 'vertices' in region._params: + frame = region.vertices.frame.name + elif 'start' in region._params: + frame = region.start.frame.name else: - if 'center' in region._params: - frame = region.center.frame.name - elif 'vertices' in region._params: - frame = region.vertices.frame.name - elif 'start' in region._params: - frame = region.start.frame.name - else: - raise ValueError(f'Unable to get coordinate frame for {region!r}') + raise ValueError(f'Unable to get coordinate frame for {region!r}') - if frame not in mapping.keys(): + if frame not in mapping: warnings.warn(f'Cannot serialize region with frame={frame}, skipping', AstropyUserWarning) @@ -185,10 +184,7 @@ def _get_region_params(region, shape_template, precision=8): elif isinstance(value, SkyCoord): val = value.to_string(precision=precision) # polygon region has multiple SkyCoord - if not value.isscalar: - value = ' '.join(val) - else: - value = val + value = ' '.join(val) if not value.isscalar else val value = value.replace(' ', ',') elif isinstance(value, Angle): diff --git a/regions/io/fits/read.py b/regions/io/fits/read.py index aa67b966..c5521d05 100644 --- a/regions/io/fits/read.py +++ b/regions/io/fits/read.py @@ -111,15 +111,15 @@ def get_column_values(region_row, colname): try: return value[index] - except IndexError: + except IndexError as exc: raise FITSParserError(f'The {colname!r} column must have more ' - f'than {index!r} values for the region.') + f'than {index!r} values for the ' + 'region.') from exc def get_shape_params(shape, region_row, shape_columns): - values = [] - for column in shape_columns: - values.append(get_column_values(region_row, column)) + values = [get_column_values(region_row, column) + for column in shape_columns] if 'rectangle' in shape: (xmin, xmax, ymin, ymax) = values[0:4] diff --git a/regions/io/fits/tests/test_fits.py b/regions/io/fits/tests/test_fits.py index 740a6821..d0c1df7c 100644 --- a/regions/io/fits/tests/test_fits.py +++ b/regions/io/fits/tests/test_fits.py @@ -36,7 +36,7 @@ def test_roundtrip(tmpdir): tempfile = tmpdir.join('tmp.fits').strpath regions.write(tempfile, format='fits', overwrite=True) regions2 = Regions.read(tempfile, format='fits') - for reg1, reg2 in zip(regions, regions2): + for reg1, reg2 in zip(regions, regions2, strict=True): assert_region_allclose(reg1, reg2) @@ -114,20 +114,20 @@ def test_components(): assert 'COMPONENT' not in tbl1.colnames components = [None, None, None, None] - for region, component in zip(regions, components): + for region, component in zip(regions, components, strict=True): region.meta = RegionMeta({'component': component}) tbl2 = regions.serialize(format='fits') assert 'COMPONENT' not in tbl2.colnames components = np.arange(4) - for region, component in zip(regions, components): + for region, component in zip(regions, components, strict=True): region.meta = RegionMeta({'component': component}) tbl3 = regions.serialize(format='fits') assert 'COMPONENT' in tbl3.colnames assert_equal(tbl3['COMPONENT'], components) components = [1, 2, None, 4] - for region, component in zip(regions, components): + for region, component in zip(regions, components, strict=True): region.meta = RegionMeta({'component': component}) tbl4 = regions.serialize(format='fits') assert 'COMPONENT' in tbl4.colnames diff --git a/regions/io/fits/write.py b/regions/io/fits/write.py index ce1ccb6b..9482767a 100644 --- a/regions/io/fits/write.py +++ b/regions/io/fits/write.py @@ -150,7 +150,7 @@ def _make_column(arrays): arr_size = np.max(arr_sizes) data = [] - for (arr, size) in zip(arrays, arr_sizes): + for (arr, size) in zip(arrays, arr_sizes, strict=True): pad_width = arr_size - size if pad_width != 0: arr = np.pad(arr, (0, pad_width), mode='constant') diff --git a/regions/shapes/circle.py b/regions/shapes/circle.py index 99870ed1..c06d33dd 100644 --- a/regions/shapes/circle.py +++ b/regions/shapes/circle.py @@ -94,7 +94,9 @@ def to_sky(self, wcs): @property def bounding_box(self): - """Bounding box (`~regions.RegionBoundingBox`).""" + """ + Bounding box (`~regions.RegionBoundingBox`). + """ xmin = self.center.x - self.radius xmax = self.center.x + self.radius ymin = self.center.y - self.radius @@ -120,10 +122,7 @@ def to_mask(self, mode='center', subpixels=1): ymin = float(bbox.iymin) - 0.5 - self.center.y ymax = float(bbox.iymax) - 0.5 - self.center.y - if mode == 'subpixels': - use_exact = 0 - else: - use_exact = 1 + use_exact = 0 if mode == 'subpixels' else 1 fraction = circular_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, self.radius, use_exact, subpixels) diff --git a/regions/shapes/ellipse.py b/regions/shapes/ellipse.py index 056182f2..570c7f96 100644 --- a/regions/shapes/ellipse.py +++ b/regions/shapes/ellipse.py @@ -1,6 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst """ -This module defines elliptical regions in both pixel and sky coordinates. +This module defines elliptical regions in both pixel and sky +coordinates. """ import math @@ -164,10 +165,7 @@ def to_mask(self, mode='center', subpixels=5): ymin = float(bbox.iymin) - 0.5 - self.center.y ymax = float(bbox.iymax) - 0.5 - self.center.y - if mode == 'subpixels': - use_exact = 0 - else: - use_exact = 1 + use_exact = 0 if mode == 'subpixels' else 1 fraction = elliptical_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, 0.5 * self.width, 0.5 * self.height, @@ -224,7 +222,7 @@ def _update_from_mpl_selector(self, *args, **kwargs): def as_mpl_selector(self, ax, active=True, sync=True, callback=None, **kwargs): """ - A matplotlib editable widget for this region + Return a matplotlib editable widget for this region (`matplotlib.widgets.EllipseSelector`). Parameters diff --git a/regions/shapes/line.py b/regions/shapes/line.py index 311be010..3e045117 100644 --- a/regions/shapes/line.py +++ b/regions/shapes/line.py @@ -70,10 +70,8 @@ def area(self): return 0 def contains(self, pixcoord): - if pixcoord.isscalar: - in_reg = False - else: - in_reg = np.zeros(pixcoord.x.shape, dtype=bool) + in_reg = (False if pixcoord.isscalar + else np.zeros(pixcoord.x.shape, dtype=bool)) if self.meta.get('include', True): return in_reg @@ -189,11 +187,8 @@ def __init__(self, start, end, meta=None, visual=None): self.visual = visual or RegionVisual() def contains(self, skycoord, wcs): # pylint: disable=unused-argument - if self.meta.get('include', True): - # lines never contain anything - return False - else: - return True + # lines never contain anything + return not self.meta.get('include', True) def to_pixel(self, wcs): start_x, start_y = wcs.world_to_pixel(self.start) diff --git a/regions/shapes/point.py b/regions/shapes/point.py index 51080400..9aecece1 100644 --- a/regions/shapes/point.py +++ b/regions/shapes/point.py @@ -75,10 +75,8 @@ def area(self): return 0.0 def contains(self, pixcoord): - if pixcoord.isscalar: - in_reg = False - else: - in_reg = np.zeros(pixcoord.x.shape, dtype=bool) + in_reg = (False if pixcoord.isscalar + else np.zeros(pixcoord.x.shape, dtype=bool)) if self.meta.get('include', True): # in_reg = False, always. Points do not include anything. @@ -177,11 +175,8 @@ def __init__(self, center, meta=None, visual=None): self.visual = visual or RegionVisual() def contains(self, skycoord, wcs): # pylint: disable=unused-argument - if self.meta.get('include', True): - # points never include anything - return False - else: - return True + # points never include anything + return not self.meta.get('include', True) def to_pixel(self, wcs): center_x, center_y = wcs.world_to_pixel(self.center) diff --git a/regions/shapes/polygon.py b/regions/shapes/polygon.py index 0442cd4a..8889564b 100644 --- a/regions/shapes/polygon.py +++ b/regions/shapes/polygon.py @@ -69,11 +69,12 @@ class PolygonPixelRegion(PixelRegion): meta = RegionMetaDescr('The meta attributes as a |RegionMeta|') visual = RegionVisualDescr('The visual attributes as a |RegionVisual|.') - def __init__(self, vertices, meta=None, visual=None, - origin=PixCoord(0, 0)): + def __init__(self, vertices, meta=None, visual=None, origin=None): self._vertices = vertices self.meta = meta or RegionMeta() self.visual = visual or RegionVisual() + if origin is None: + origin = PixCoord(0, 0) self.origin = origin self.vertices = vertices + origin @@ -126,10 +127,7 @@ def to_mask(self, mode='center', subpixels=5): mode = 'subpixels' subpixels = 1 - if mode == 'subpixels': - use_exact = 0 - else: - use_exact = 1 + use_exact = 0 if mode == 'subpixels' else 1 # Find bounding box and mask size bbox = self.bounding_box diff --git a/regions/shapes/rectangle.py b/regions/shapes/rectangle.py index 10ad2880..87a8edfd 100644 --- a/regions/shapes/rectangle.py +++ b/regions/shapes/rectangle.py @@ -1,6 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst """ -This module defines rectangular regions in both pixel and sky coordinates. +This module defines rectangular regions in both pixel and sky +coordinates. """ import astropy.units as u @@ -159,10 +160,7 @@ def to_mask(self, mode='center', subpixels=5): ymin = float(bbox.iymin) - 0.5 - self.center.y ymax = float(bbox.iymax) - 0.5 - self.center.y - if mode == 'subpixels': - use_exact = 0 - else: - use_exact = 1 + use_exact = 0 if mode == 'subpixels' else 1 fraction = rectangular_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, self.width, self.height, @@ -220,7 +218,7 @@ def _update_from_mpl_selector(self, *args, **kwargs): def as_mpl_selector(self, ax, active=True, sync=True, callback=None, **kwargs): """ - A matplotlib editable widget for the region + Return a matplotlib editable widget for the region (`matplotlib.widgets.RectangleSelector`). Parameters diff --git a/regions/shapes/tests/test_common.py b/regions/shapes/tests/test_common.py index 97fe7964..74e6435f 100644 --- a/regions/shapes/tests/test_common.py +++ b/regions/shapes/tests/test_common.py @@ -55,17 +55,17 @@ def test_contains_scalar(self): assert pixcoord not in self.reg def test_contains_array_1d(self): - pixcoord = PixCoord(*zip(*(self.inside + self.outside))) + pixcoord = PixCoord(*zip(*(self.inside + self.outside), strict=True)) actual = self.reg.contains(pixcoord) assert_equal(actual[:len(self.inside)], True) assert_equal(actual[len(self.inside):], False) with pytest.raises(ValueError) as excinfo: - pixcoord in self.reg + assert pixcoord in self.reg assert 'coord must be scalar' in str(excinfo.value) def test_contains_array_2d(self): - x, y = zip(*(self.inside + self.outside)) + x, y = zip(*(self.inside + self.outside), strict=True) pixcoord = PixCoord([x] * 3, [y] * 3) actual = self.reg.contains(pixcoord) diff --git a/regions/shapes/tests/test_ellipse.py b/regions/shapes/tests/test_ellipse.py index 30e5256b..574c99ff 100644 --- a/regions/shapes/tests/test_ellipse.py +++ b/regions/shapes/tests/test_ellipse.py @@ -116,7 +116,8 @@ def test_as_mpl_selector(self, sync): plt = pytest.importorskip('matplotlib.pyplot') from matplotlib.testing.widgets import do_event - data = np.random.random((16, 16)) + rng = np.random.default_rng(0) + data = rng.random((16, 16)) mask = np.zeros_like(data) ax = plt.subplot(1, 1, 1) @@ -168,12 +169,14 @@ def update_mask(reg): @pytest.mark.parametrize('anywhere', (False, True)) def test_mpl_selector_drag(self, anywhere): - """Test dragging of entire region from central handle and anywhere.""" - + """ + Test dragging of entire region from central handle and anywhere. + """ plt = pytest.importorskip('matplotlib.pyplot') from matplotlib.testing.widgets import do_event - data = np.random.random((16, 16)) + rng = np.random.default_rng(0) + data = rng.random((16, 16)) mask = np.zeros_like(data) ax = plt.subplot(1, 1, 1) @@ -225,11 +228,13 @@ def update_mask(reg): {'props': {'facecolor': 'blue', 'linewidth': 2}}, {'twit': 'gumby'})) def test_mpl_selector_kwargs(self, userargs): - """Test that additional kwargs are passed to selector.""" - + """ + Test that additional kwargs are passed to selector. + """ plt = pytest.importorskip('matplotlib.pyplot') - data = np.random.random((16, 16)) + rng = np.random.default_rng(0) + data = rng.random((16, 16)) mask = np.zeros_like(data) ax = plt.subplot(1, 1, 1) diff --git a/regions/shapes/tests/test_masks.py b/regions/shapes/tests/test_masks.py index 8c68a167..954dce9f 100644 --- a/regions/shapes/tests/test_masks.py +++ b/regions/shapes/tests/test_masks.py @@ -1,6 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst """ -This file sets up detailed tests for computing masks with reference images. +This file sets up detailed tests for computing masks with reference +images. """ import itertools diff --git a/regions/shapes/tests/test_rectangle.py b/regions/shapes/tests/test_rectangle.py index a9d7939f..535b198c 100644 --- a/regions/shapes/tests/test_rectangle.py +++ b/regions/shapes/tests/test_rectangle.py @@ -122,7 +122,8 @@ def test_as_mpl_selector(self, sync): plt = pytest.importorskip('matplotlib.pyplot') from matplotlib.testing.widgets import do_event - data = np.random.random((16, 16)) + rng = np.random.default_rng(0) + data = rng.random((16, 16)) mask = np.zeros_like(data) ax = plt.subplot(1, 1, 1) @@ -172,13 +173,15 @@ def update_mask(reg): @pytest.mark.parametrize('anywhere', (False, True)) def test_mpl_selector_drag(self, anywhere): - """Test dragging of entire region from central handle and anywhere.""" - + """ + Test dragging of entire region from central handle and anywhere. + """ plt = pytest.importorskip('matplotlib.pyplot') from matplotlib.testing.widgets import ( do_event) # click_and_drag # MPL_VERSION >= 36 - data = np.random.random((16, 16)) + rng = np.random.default_rng(0) + data = rng.random((16, 16)) mask = np.zeros_like(data) ax = plt.subplot(1, 1, 1) @@ -230,11 +233,13 @@ def update_mask(reg): {'props': {'facecolor': 'blue', 'linewidth': 2}}, {'twit': 'gumby'})) def test_mpl_selector_kwargs(self, userargs): - """Test that additional kwargs are passed to selector.""" - + """ + Test that additional kwargs are passed to selector. + """ plt = pytest.importorskip('matplotlib.pyplot') - data = np.random.random((16, 16)) + rng = np.random.default_rng(0) + data = rng.random((16, 16)) mask = np.zeros_like(data) ax = plt.subplot(1, 1, 1)