|
11 | 11 | nir.xrs.ndvi(red) |
12 | 12 | """ |
13 | 13 |
|
| 14 | +import functools |
| 15 | +import html |
| 16 | +import inspect |
| 17 | +import re |
| 18 | +import textwrap |
14 | 19 | import warnings |
15 | 20 |
|
16 | 21 | import xarray as xr |
@@ -579,13 +584,120 @@ def _open_geotiff_windowed(obj, source, *, auto_reproject=False, |
579 | 584 | return result |
580 | 585 |
|
581 | 586 |
|
| 587 | +# --------------------------------------------------------------------------- |
| 588 | +# Accessor repr: list the available ``.xrs`` operations grouped by category so |
| 589 | +# users can discover the toolset from a plain REPL or notebook, without relying |
| 590 | +# on tab-completion or an LSP (issue #3476). |
| 591 | +# --------------------------------------------------------------------------- |
| 592 | + |
| 593 | +# A category banner in the accessor source, e.g. `` # ---- Hydrology ----``. |
| 594 | +_REPR_BANNER_RE = re.compile(r'^\s*#\s*----\s*(.+?)\s*----\s*$') |
| 595 | +# A method definition inside a class body (indented ``def name(``). |
| 596 | +_REPR_METHOD_RE = re.compile(r'^\s+def\s+([A-Za-z][A-Za-z0-9_]*)\s*\(') |
| 597 | + |
| 598 | + |
| 599 | +@functools.lru_cache(maxsize=None) |
| 600 | +def _accessor_categories(cls): |
| 601 | + """Parse ``# ---- Category ----`` banners out of *cls* source. |
| 602 | +
|
| 603 | + Returns an ordered tuple of ``(category, (method_name, ...))`` pairs, |
| 604 | + one per banner that has at least one public method beneath it. Returns |
| 605 | + an empty tuple when the source is unavailable (e.g. a frozen build), so |
| 606 | + callers fall back to a flat listing. |
| 607 | + """ |
| 608 | + try: |
| 609 | + source = inspect.getsource(cls) |
| 610 | + except (OSError, TypeError): |
| 611 | + return () |
| 612 | + categories = [] |
| 613 | + current = None |
| 614 | + for line in source.splitlines(): |
| 615 | + banner = _REPR_BANNER_RE.match(line) |
| 616 | + if banner: |
| 617 | + current = (banner.group(1), []) |
| 618 | + categories.append(current) |
| 619 | + continue |
| 620 | + method = _REPR_METHOD_RE.match(line) |
| 621 | + if method and current is not None and not method.group(1).startswith('_'): |
| 622 | + current[1].append(method.group(1)) |
| 623 | + return tuple( |
| 624 | + (name, tuple(methods)) for name, methods in categories if methods |
| 625 | + ) |
| 626 | + |
| 627 | + |
| 628 | +def _public_accessor_methods(cls): |
| 629 | + """Sorted public method names defined directly on *cls*.""" |
| 630 | + return tuple(sorted( |
| 631 | + name for name, member in vars(cls).items() |
| 632 | + if callable(member) and not name.startswith('_') |
| 633 | + )) |
| 634 | + |
| 635 | + |
| 636 | +def _accessor_kind(cls): |
| 637 | + return 'Dataset' if cls.__name__.endswith('DatasetAccessor') else 'DataArray' |
| 638 | + |
| 639 | + |
| 640 | +def _accessor_repr_text(cls): |
| 641 | + """Plain-text repr listing the accessor's operations by category.""" |
| 642 | + lines = [ |
| 643 | + f"<{cls.__name__}> xarray-spatial tools for this {_accessor_kind(cls)}", |
| 644 | + "call as: .xrs.<name>(...)", |
| 645 | + "", |
| 646 | + ] |
| 647 | + categories = _accessor_categories(cls) |
| 648 | + if not categories: |
| 649 | + lines.append("(categories unavailable; flat method list)") |
| 650 | + categories = (("", _public_accessor_methods(cls)),) |
| 651 | + for name, methods in categories: |
| 652 | + if name: |
| 653 | + lines.append(f"{name}:") |
| 654 | + for chunk in textwrap.wrap( |
| 655 | + ", ".join(methods), width=74, |
| 656 | + break_long_words=False, break_on_hyphens=False, |
| 657 | + ): |
| 658 | + lines.append(f" {chunk}") |
| 659 | + return "\n".join(lines) |
| 660 | + |
| 661 | + |
| 662 | +def _accessor_repr_html(cls): |
| 663 | + """Jupyter HTML repr: the same listing as a compact table.""" |
| 664 | + categories = _accessor_categories(cls) |
| 665 | + if not categories: |
| 666 | + categories = (("Available tools", _public_accessor_methods(cls)),) |
| 667 | + rows = [] |
| 668 | + for name, methods in categories: |
| 669 | + methods_html = ", ".join( |
| 670 | + f"<code>{html.escape(m)}</code>" for m in methods |
| 671 | + ) |
| 672 | + rows.append( |
| 673 | + "<tr>" |
| 674 | + "<td style='text-align:right;vertical-align:top;" |
| 675 | + "padding-right:0.75em;font-weight:bold;white-space:nowrap'>" |
| 676 | + f"{html.escape(name)}</td>" |
| 677 | + f"<td style='text-align:left'>{methods_html}</td>" |
| 678 | + "</tr>" |
| 679 | + ) |
| 680 | + return ( |
| 681 | + f"<div><strong>xarray-spatial</strong> tools for this " |
| 682 | + f"{_accessor_kind(cls)} — call as " |
| 683 | + "<code>.xrs.<name>(...)</code>" |
| 684 | + f"<table>{''.join(rows)}</table></div>" |
| 685 | + ) |
| 686 | + |
| 687 | + |
582 | 688 | @xr.register_dataarray_accessor("xrs") |
583 | 689 | class XrsSpatialDataArrayAccessor: |
584 | 690 | """DataArray accessor exposing xarray-spatial operations.""" |
585 | 691 |
|
586 | 692 | def __init__(self, obj): |
587 | 693 | self._obj = obj |
588 | 694 |
|
| 695 | + def __repr__(self): |
| 696 | + return _accessor_repr_text(type(self)) |
| 697 | + |
| 698 | + def _repr_html_(self): |
| 699 | + return _accessor_repr_html(type(self)) |
| 700 | + |
589 | 701 | # ---- Plot ---- |
590 | 702 |
|
591 | 703 | def plot(self, **kwargs): |
@@ -1238,6 +1350,12 @@ class XrsSpatialDatasetAccessor: |
1238 | 1350 | def __init__(self, obj): |
1239 | 1351 | self._obj = obj |
1240 | 1352 |
|
| 1353 | + def __repr__(self): |
| 1354 | + return _accessor_repr_text(type(self)) |
| 1355 | + |
| 1356 | + def _repr_html_(self): |
| 1357 | + return _accessor_repr_html(type(self)) |
| 1358 | + |
1241 | 1359 | # ---- Plot ---- |
1242 | 1360 |
|
1243 | 1361 | def plot(self, vars=None, cols=3, **kwargs): |
|
0 commit comments