diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f249704..e89b82af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,10 +20,9 @@ repos: - id: check-json - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.3 + rev: v0.1.6 hooks: - id: ruff - args: [geoviews] files: geoviews/ - repo: https://github.com/hoxbro/clean_notebook rev: v0.1.13 @@ -35,3 +34,13 @@ repos: - id: codespell additional_dependencies: - tomli + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.9.0.6 + hooks: + - id: shellcheck diff --git a/doc/index.rst b/doc/index.rst index 84e0f6df..9b8f990a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -40,9 +40,9 @@ notebook:: cd geoviews-examples jupyter notebook -(Here `geoviews examples` is a shorthand for `geoviews copy-examples +(Here ``geoviews examples`` is a shorthand for ``geoviews copy-examples --path geoviews-examples && geoviews fetch-data --path -geoviews-examples`.) +geoviews-examples``.) In the classic Jupyter notebook environment and JupyterLab, first make sure to load the ``gv.extension()``. For versions of @@ -59,8 +59,8 @@ Once you have installed JupyterLab and the extension launch it with:: jupyter-lab If you want to try out the latest features between releases, you can -get the latest dev release by specifying `-c pyviz/label/dev` in place -of `-c pyviz`. +get the latest dev release by specifying ``-c pyviz/label/dev`` in place +of ``-c pyviz``. Additional dependencies ======================= diff --git a/doc/releases.rst b/doc/releases.rst index 5f2a13ba..f5c3c0b9 100644 --- a/doc/releases.rst +++ b/doc/releases.rst @@ -116,8 +116,8 @@ instead. The ``Wikipedia`` tile source will be removed in version ``rioxarray.open_rasterio`` to load GeoTIFFs into a ``xarray.DataArray``. -Note, this release has a minor breaking change where `gv.feature.states` -defaults to `fill_color=None` so the fill color is transparent. +Note, this release has a minor breaking change where ``gv.feature.states`` +defaults to ``fill_color=None`` so the fill color is transparent. Enhancements: @@ -358,7 +358,7 @@ Features: * Added geographic projection awareness to ``hv.annotate`` function (`#377 `_, `#419 `_) * Rewrote geometry interfaces such as geopandas to conform to new HoloViews geometry protocol (`#407 `_) * Implement consistent .geom method on geometry types (e.g. Path, Polygons, Points) (`#424 `_) -* Add new `Rectangles` and `Segments` elements (`#377 `_) +* Add new ``Rectangles`` and ``Segments`` elements (`#377 `_) Bug fixes: diff --git a/geoviews/data/geom_dict.py b/geoviews/data/geom_dict.py index 0d22091a..6b325b9b 100644 --- a/geoviews/data/geom_dict.py +++ b/geoviews/data/geom_dict.py @@ -285,7 +285,9 @@ def iloc(cls, dataset, index): return data @classmethod - def sample(cls, dataset, samples=[]): + def sample(cls, dataset, samples=None): + if samples is None: + samples = [] raise NotImplementedError('sampling operation not implemented for geometries.') @classmethod diff --git a/geoviews/data/geopandas.py b/geoviews/data/geopandas.py index 792a9a19..7ddf88f8 100644 --- a/geoviews/data/geopandas.py +++ b/geoviews/data/geopandas.py @@ -44,7 +44,7 @@ def geo_column(cls, data): except AttributeError: if len(data): raise ValueError('No geometry column found in geopandas.DataFrame, ' - 'use the PandasInterface instead.') + 'use the PandasInterface instead.') from None return None @classmethod @@ -314,12 +314,16 @@ def reindex(cls, dataset, kdims=None, vdims=None): return dataset.data @classmethod - def sample(cls, columns, samples=[]): + def sample(cls, columns, samples=None): + if samples is None: + samples = [] raise NotImplementedError @classmethod - def sort(cls, dataset, by=[], reverse=False): + def sort(cls, dataset, by=None, reverse=False): + if by is None: + by = [] geo_dims = cls.geom_dims(dataset) if any(d in geo_dims for d in by): raise DataError("SpatialPandasInterface does not allow sorting " @@ -398,7 +402,7 @@ def values(cls, dataset, dimension, expanded=True, flat=True, compute=True, keep ds = dataset.clone(dict_data, datatype=['geom_dictionary']) values = [] geom_type = data.geom_type.iloc[0] - for i, row in data.iterrows(): + for _i, row in data.iterrows(): ds.data = row.to_dict() if new_geo_col_name != default_geo_name: ds.data[new_geo_col_name] = ds.data.pop(default_geo_name) @@ -496,7 +500,7 @@ def split(cls, dataset, start, end, datatype, **kwargs): d.update({vd.name: row[vd.name] for vd in dataset.vdims}) geom_type = cls.geom_type(dataset) ds = dataset.clone([d], datatype=['multitabular']) - for i, row in dataset.data.iterrows(): + for _i, row in dataset.data.iterrows(): if datatype == 'geom': objs.append(row[col]) continue @@ -544,7 +548,7 @@ def get_geom_type(geom): return 'Polygon' -def to_geopandas(data, xdim, ydim, columns=[], geom='point'): +def to_geopandas(data, xdim, ydim, columns=None, geom='point'): """Converts list of dictionary format geometries to spatialpandas line geometries. Args: @@ -560,6 +564,8 @@ def to_geopandas(data, xdim, ydim, columns=[], geom='point'): from shapely.geometry import ( Point, LineString, Polygon, MultiPoint, MultiPolygon, MultiLineString ) + if columns is None: + columns = [] poly = any('holes' in d for d in data) or geom == 'Polygon' if poly: single_type, multi_type = Polygon, MultiPolygon diff --git a/geoviews/data/iris.py b/geoviews/data/iris.py index 2552751f..9322fcaa 100644 --- a/geoviews/data/iris.py +++ b/geoviews/data/iris.py @@ -356,10 +356,12 @@ def length(cls, dataset): @classmethod - def sort(cls, columns, by=[], reverse=False): + def sort(cls, columns, by=None, reverse=False): """ Cubes are assumed to be sorted by default. """ + if by is None: + by = [] return columns @@ -372,10 +374,12 @@ def aggregate(cls, columns, kdims, function, **kwargs): @classmethod - def sample(cls, dataset, samples=[]): + def sample(cls, dataset, samples=None): """ Sampling currently not implemented. """ + if samples is None: + samples = [] raise NotImplementedError diff --git a/geoviews/element/geo.py b/geoviews/element/geo.py index 99bc40ec..12b46a43 100644 --- a/geoviews/element/geo.py +++ b/geoviews/element/geo.py @@ -928,7 +928,7 @@ def from_shapefile(cls, shapefile, *args, **kwargs): @classmethod def from_records(cls, records, dataset=None, on=None, value=None, - index=[], drop_missing=False, element=None, **kwargs): + index=None, drop_missing=False, element=None, **kwargs): """ Load data from a collection of `cartopy.io.shapereader.Record` objects and optionally merge it with a dataset to assign @@ -965,6 +965,8 @@ def from_records(cls, records, dataset=None, on=None, value=None, shapes: Polygons or Path object A Polygons or Path object containing the geometries """ + if index is None: + index = [] if dataset is not None and not on: raise ValueError('To merge dataset with shapes mapping ' 'must define attribute(s) to merge on.') @@ -1002,7 +1004,7 @@ def from_records(cls, records, dataset=None, on=None, value=None, vdims = [] data = [] - for i, rec in enumerate(records): + for rec in records: geom = {} if dataset: selection = {dim: rec.attributes.get(attr, None) diff --git a/geoviews/operation/regrid.py b/geoviews/operation/regrid.py index 23905ed5..c2512219 100644 --- a/geoviews/operation/regrid.py +++ b/geoviews/operation/regrid.py @@ -53,7 +53,7 @@ def _get_regridder(self, element): try: import xesmf as xe except ImportError: - raise ImportError("xESMF library required for weighted regridding.") + raise ImportError("xESMF library required for weighted regridding.") from None x, y = element.kdims if self.p.target: tx, ty = self.p.target.kdims[:2] diff --git a/geoviews/plotting/bokeh/plot.py b/geoviews/plotting/bokeh/plot.py index 3ef5c72d..af85c2ff 100644 --- a/geoviews/plotting/bokeh/plot.py +++ b/geoviews/plotting/bokeh/plot.py @@ -71,7 +71,9 @@ def __init__(self, element, **params): ) def _axis_properties(self, axis, key, plot, dimension=None, - ax_mapping={'x': 0, 'y': 1}): + ax_mapping=None): + if ax_mapping is None: + ax_mapping = {'x': 0, 'y': 1} axis_props = super()._axis_properties(axis, key, plot, dimension, ax_mapping) proj = self.projection @@ -115,7 +117,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): def _postprocess_hover(self, renderer, source): super()._postprocess_hover(renderer, source) - hover = getattr(self.handles["plot"], "hover") + hover = self.handles["plot"].hover hover = hover[0] if hover else None if (not self.geographic or hover is None or isinstance(hover.tooltips, str) or self.projection is not GOOGLE_MERCATOR diff --git a/geoviews/streams.py b/geoviews/streams.py index c0c630b5..613ca150 100644 --- a/geoviews/streams.py +++ b/geoviews/streams.py @@ -15,7 +15,11 @@ class PolyVertexEdit(PolyEdit): A dictionary specifying the style options for the intermediate nodes. """ - def __init__(self, node_style={}, feature_style={}, **params): + def __init__(self, node_style=None, feature_style=None, **params): + if feature_style is None: + feature_style = {} + if node_style is None: + node_style = {} self.node_style = node_style self.feature_style = feature_style super().__init__(**params) @@ -35,7 +39,11 @@ class PolyVertexDraw(PolyDraw): A dictionary specifying the style options for the intermediate nodes. """ - def __init__(self, node_style={}, feature_style={}, **params): + def __init__(self, node_style=None, feature_style=None, **params): + if feature_style is None: + feature_style = {} + if node_style is None: + node_style = {} self.node_style = node_style self.feature_style = feature_style super().__init__(**params) diff --git a/geoviews/tests/data/test_iris.py b/geoviews/tests/data/test_iris.py index e7e8437a..6a274a80 100644 --- a/geoviews/tests/data/test_iris.py +++ b/geoviews/tests/data/test_iris.py @@ -7,7 +7,7 @@ from iris.tests.stock import lat_lon_cube from iris.exceptions import MergeError except ImportError: - raise SkipTest("Could not import iris, skipping IrisInterface tests.") + raise SkipTest("Could not import iris, skipping IrisInterface tests.") from None from holoviews.core.data import Dataset, concat from geoviews.data.iris import coord_to_dimension diff --git a/geoviews/tests/test_conversions.py b/geoviews/tests/test_conversions.py index aec36af3..357612e2 100644 --- a/geoviews/tests/test_conversions.py +++ b/geoviews/tests/test_conversions.py @@ -3,7 +3,7 @@ try: from iris.tests.stock import lat_lon_cube except ImportError: - raise unittest.SkipTest("iris not available") + raise unittest.SkipTest("iris not available") from None from holoviews.core import HoloMap from holoviews.element import Curve diff --git a/geoviews/util.py b/geoviews/util.py index da2a62b8..91ba8a8a 100644 --- a/geoviews/util.py +++ b/geoviews/util.py @@ -98,14 +98,14 @@ def project_extents(extents, src_proj, dest_proj, tol=1e-6): geom_in_src_proj = geom_clipped_to_dest_proj try: geom_in_crs = dest_proj.project_geometry(geom_in_src_proj, src_proj) - except ValueError: + except ValueError as e: src_name =type(src_proj).__name__ dest_name =type(dest_proj).__name__ raise ValueError( f'Could not project data from {src_name} projection ' f'to {dest_name} projection. Ensure the coordinate ' 'reference system (crs) matches your data and the kdims.' - ) + ) from e else: geom_in_crs = boundary_poly.intersection(domain_in_src_proj) return geom_in_crs.bounds @@ -139,12 +139,14 @@ def zoom(mapPx, worldPx, fraction): return int(zoom) if np.isfinite(zoom) else 0 -def geom_dict_to_array_dict(geom_dict, coord_names=['Longitude', 'Latitude']): +def geom_dict_to_array_dict(geom_dict, coord_names=None): """ Converts a dictionary containing an geometry key to a dictionary of x- and y-coordinate arrays and if present a list-of-lists of hole array. """ + if coord_names is None: + coord_names = ["Longitude", "Latitude"] x, y = coord_names geom = geom_dict['geometry'] new_dict = {k: v for k, v in geom_dict.items() if k != 'geometry'} @@ -266,19 +268,19 @@ def polygons_to_geom_dicts(polygons, skip_invalid=True): return polys -def path_to_geom_dicts(path, skip_invalid=True): +def path_to_geom_dicts(fullpath, skip_invalid=True): """ Converts a Path element into a list of geometry dictionaries, preserving all value dimensions. """ - geoms = unpack_geoms(path) + geoms = unpack_geoms(fullpath) if geoms is not None: return geoms geoms = [] invalid = False - xdim, ydim = path.kdims - for i, path in enumerate(path.split(datatype='columns')): + xdim, ydim = fullpath.kdims + for path in fullpath.split(datatype='columns'): array = np.column_stack([path.pop(xdim.name), path.pop(ydim.name)]) splits = np.where(np.isnan(array[:, :2].astype('float')).sum(axis=1))[0] arrays = np.split(array, splits+1) if len(splits) else [array] @@ -581,7 +583,7 @@ def process_crs(crs): import cartopy.crs as ccrs import pyproj except ImportError: - raise ImportError('Geographic projection support requires pyproj and cartopy.') + raise ImportError('Geographic projection support requires pyproj and cartopy.') from None if crs is None: return ccrs.PlateCarree() @@ -641,7 +643,7 @@ def load_tiff(filename, crs=None, apply_transform=False, nan_nodata=False, **kwa try: import xarray as xr except ImportError: - raise ImportError('Loading tiffs requires xarray to be installed') + raise ImportError('Loading tiffs requires xarray to be installed') from None try: with warnings.catch_warnings(): warnings.filterwarnings('ignore') @@ -751,7 +753,7 @@ def from_xarray(da, crs=None, apply_transform=False, nan_nodata=False, **kwargs) return el -def get_tile_rgb(tile_source, bbox, zoom_level, bbox_crs=ccrs.PlateCarree()): +def get_tile_rgb(tile_source, bbox, zoom_level, bbox_crs=None): """ Returns an RGB element given a tile_source, bounding box and zoom level. @@ -773,6 +775,9 @@ def get_tile_rgb(tile_source, bbox, zoom_level, bbox_crs=ccrs.PlateCarree()): """ from .element import RGB, WMTS + if bbox_crs is None: + bbox_crs = ccrs.PlateCarree() + if isinstance(tile_source, (WMTS, Tiles)): tile_source = tile_source.data diff --git a/pyproject.toml b/pyproject.toml index 658c478d..41487451 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,11 @@ requires = [ build-backend = "setuptools.build_meta" [tool.ruff] -target-version = "py39" +fix = true +[tool.ruff.lint] select = [ + "B", "E", "F", "FLY", @@ -35,7 +37,6 @@ ignore = [ "E731", # Do not assign a lambda expression, use a def "E741", # Ambiguous variable name "F405", # From star imports - "PLC1901", # empty string is falsey "PLE0604", # Invalid object in `__all__`, must contain only strings "PLE0605", # Invalid format for `__all__` "PLR091", # Too many arguments/branches/statements @@ -45,12 +46,11 @@ ignore = [ "RUF012", # Mutable class attributes should use `typing.ClassVar` ] -fix = true unfixable = [ "F401", # Unused imports ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F403"] "geoviews/tests/*" = [ "RUF001", "RUF002", "RUF003", # Ambiguous unicode character diff --git a/tox.ini b/tox.ini index f29dea60..bb50caa6 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ [tox] # python version test group extra envs extra commands -envlist = {py39,py310,py311,py312}-{flakes,unit,examples,all_recommended,examples_extra,simple}-{default}-{dev,pkg} +envlist = {py39,py310,py311,py312}-{unit,examples,all_recommended,examples_extra,simple}-{default}-{dev,pkg} [_simple] description = Install geoviews without any optional dependencies @@ -11,12 +11,6 @@ deps = . commands = python -c "import geoviews as gv; print(gv.__version__)" -[_flakes] -description = Flake check python and notebooks -deps = .[tests] -commands = flake8 -# pytest --nbsmoke-lint -k ".ipynb" - [_unit] description = Run unit tests with coverage deps = .[tests] @@ -35,8 +29,7 @@ commands = pytest --nbsmoke-run -k ".ipynb" --ignore-nbsmoke-skip-run [_all_recommended] description = Run all recommended tests deps = .[recommended, tests] -commands = {[_flakes]commands} - {[_unit]commands} +commands = {[_unit]commands} {[_examples]commands} [_pkg] @@ -51,21 +44,22 @@ changedir = {envtmpdir} commands = examples-pkg: {[_pkg]commands} unit: {[_unit]commands} - flakes: {[_flakes]commands} examples: {[_examples]commands} examples_extra: {[_examples_extra]commands} simple: {[_simple]commands} all_recommended: {[_all_recommended]commands} deps = unit: {[_unit]deps} - flakes: {[_flakes]deps} examples: {[_examples]deps} examples_extra: {[_examples_extra]deps} all_recommended: {[_all_recommended]deps} [pytest] -addopts = -v --pyargs --doctest-ignore-import-errors +addopts = -v --pyargs --doctest-ignore-import-errors --strict-markers --color=yes norecursedirs = doc .git dist build _build .ipynb_checkpoints +minversion = 7 +xfail_strict = true +log_cli_level = INFO # depend on optional iris, xesmf, etc nbsmoke_skip_run = .*Homepage\.ipynb$ .*user_guide/Resampling_Grids\.ipynb$ @@ -87,12 +81,3 @@ filterwarnings = error::geoviews.GeoviewsUserWarning ; 2023-09: See https://github.com/Unidata/MetPy/pull/3117 ignore:'xdrlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:metpy.io.nexrad - - -[flake8] -include = *.py -# run_tests.py is generated by conda build, which appears to have a -# bug resulting in code being duplicated a couple of times. -exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,.ipynb_checkpoints,run_test.py -ignore = E, - W