Repository: Zeroto521/my-data-toolkit

Issue:
EHN: new accessor method `get_attr`



Discussion:
# [Codecov](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) Report
> Merging [#379](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) (20f78e3) into [master](https://codecov.io/gh/Zeroto521/my-data-toolkit/commit/8690378ad990f85e7c9aa408194f01d611a5466d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) (8690378) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379/graphs/tree.svg?width=650&height=150&src=pr&token=ukBguscD8E&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero)](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero)

```diff
@@           Coverage Diff           @@
##           master     #379   +/-   ##
=======================================
  Coverage   99.80%   99.81%           
=======================================
  Files          23       23           
  Lines         521      529    +8     
  Branches       73       73           
=======================================
+ Hits          520      528    +8     
  Partials        1        1           
```


| [Impacted Files](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) | Coverage Δ | |
|---|---|---|
| [dtoolkit/accessor/series.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero#diff-ZHRvb2xraXQvYWNjZXNzb3Ivc2VyaWVzLnB5) | `100.00% <100.00%> (ø)` | |
| [dtoolkit/geoaccessor/geoseries.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero#diff-ZHRvb2xraXQvZ2VvYWNjZXNzb3IvZ2Vvc2VyaWVzLnB5) | `100.00% <100.00%> (ø)` | |
| [dtoolkit/transformer/factory.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero#diff-ZHRvb2xraXQvdHJhbnNmb3JtZXIvZmFjdG9yeS5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero). Last update [8690378...20f78e3](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/379?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero).


Resolving patch:
diff --git a/dtoolkit/accessor/register.py b/dtoolkit/accessor/register.py
--- a/dtoolkit/accessor/register.py
+++ b/dtoolkit/accessor/register.py
@@ -8,6 +8,8 @@
 from pandas.util._decorators import doc
 
 if TYPE_CHECKING:
+    from typing import Callable
+
     from dtoolkit._typing import SeriesOrFrame
 
 
@@ -19,11 +21,13 @@ def register_method_factory(register_accessor):
     --------
     register_series_method
     register_dataframe_method
+    dtoolkit.geoaccessor.register_geoseries_method
+    dtoolkit.geoaccessor.register_geodataframe_method
     """
 
     # based on pandas_flavor/register.py
     @wraps(register_accessor)
-    def register_accessor_method(method):
+    def register_accessor_method(method: Callable, name: str):
         @wraps(method)
         def method_accessor(pd_obj: SeriesOrFrame):
             @wraps(method)
@@ -32,22 +36,45 @@ def wrapper(*args, **kwargs):
 
             return wrapper
 
-        register_accessor(method.__name__)(method_accessor)
+        # Register method as pandas object inner method.
+        register_accessor(name)(method_accessor)
 
         # Must return method itself, otherwise would get None.
         return method
 
-    return register_accessor_method
+    @wraps(register_accessor)
+    def register_accessor_alias(name: str | None = None):
+        @wraps(register_accessor)
+        def wrapper(method: Callable):
+            return register_accessor_method(method, name or method.__name__)
+
+        return wrapper
+
+    @wraps(register_accessor)
+    def decorator(name: Callable | str | None = None):
+        if callable(name):  # Supports `@register_*_method` using.
+            method = name  # This 'name' variable actually is a function.
+            return register_accessor_method(method, method.__name__)
+
+        # Supports `@register_*_method()` and `@register_*_method(name="")` using.
+        return register_accessor_alias(name)
+
+    return decorator
 
 
 @register_method_factory
-@doc(klass=":class:`pandas.Series`")
-def register_series_method(method):
+@doc(klass=":class:`~pandas.Series`")
+def register_series_method(name: str | None = None):
     """
     {klass} register accessor for human.
 
     Write method normally, use method naturally.
 
+    Parameters
+    ----------
+    name : str, optional
+        Use the ``method`` name as the default accessor entrance if ``name`` is None.
+
     See Also
     --------
     register_series_method
@@ -61,8 +88,10 @@ def register_series_method(method):
 
         import pandas as pd
 
+        @register_dataframe_method(name="col")
+        @register_series_method(name="col")  # Support alias name also.
         @register_dataframe_method
-        @register_series_method
+        @register_series_method  # Use accessor method `__name__` as the entrance.
         def cols(pd_obj):
             '''
             An API to gather :attr:`~pandas.Series.name` and
@@ -88,11 +117,19 @@ def cols(pd_obj):
         In [4]: df.a.cols()
         Out[4]:
         'a'
+
+        In [5]: df.col()
+        Out[5]:
+        ['a', 'b']
+
+        In [6]: df.a.col()
+        Out[6]:
+        'a'
     """
-    return register_series_accessor(method)
+    return register_series_accessor(name)
 
 
 @register_method_factory
-@doc(register_series_method, klass=":class:`pandas.DataFrame`")
-def register_dataframe_method(method):
-    return register_dataframe_accessor(method)
+@doc(register_series_method, klass=":class:`~pandas.DataFrame`")
+def register_dataframe_method(name: str | None = None):
+    return register_dataframe_accessor(name)
diff --git a/dtoolkit/geoaccessor/accessor.py b/dtoolkit/geoaccessor/accessor.py
--- a/dtoolkit/geoaccessor/accessor.py
+++ b/dtoolkit/geoaccessor/accessor.py
@@ -1,10 +1,9 @@
-from geopandas import GeoDataFrame
-from geopandas import GeoSeries
+import geopandas as gpd
 from pandas.core.accessor import _register_accessor
 from pandas.util._decorators import doc
 
 
-@doc(klass=":class:`geopandas.GeoSeries`")
+@doc(klass=":class:`~geopandas.GeoSeries`")
 def register_geoseries_accessor(name: str):
     """
     Register a custom accessor on {klass} objects.
@@ -108,10 +107,10 @@ def count_coordinates(self):
         Name: geometry, dtype: int64
     """
 
-    return _register_accessor(name, GeoSeries)
+    return _register_accessor(name, gpd.GeoSeries)
 
 
-@doc(register_geoseries_accessor, klass=":class:`geopandas.GeoDataFrame`")
+@doc(register_geoseries_accessor, klass=":class:`~geopandas.GeoDataFrame`")
 def register_geodataframe_accessor(name: str):
 
-    return _register_accessor(name, GeoDataFrame)
+    return _register_accessor(name, gpd.GeoDataFrame)
diff --git a/dtoolkit/geoaccessor/register.py b/dtoolkit/geoaccessor/register.py
--- a/dtoolkit/geoaccessor/register.py
+++ b/dtoolkit/geoaccessor/register.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from pandas.util._decorators import doc
 
 from dtoolkit.accessor.register import register_method_factory
@@ -6,13 +8,18 @@
 
 
 @register_method_factory
-@doc(klass=":class:`geopandas.GeoSeries`")
-def register_geoseries_method(method):
+@doc(klass=":class:`~geopandas.GeoSeries`")
+def register_geoseries_method(name: str | None = None):
     """
     {klass} register accessor for human.
 
     Write method normally, use method naturally.
 
+    Parameters
+    ----------
+    name : str, optional
+        Use the ``method`` name as the default accessor entrance if ``name`` is None.
+
     See Also
     --------
     dtoolkit.geoaccessor.register_geoseries_accessor
@@ -28,8 +35,10 @@ def register_geoseries_method(method):
 
         from pygeos import count_coordinates, from_shapely
 
+        @register_geodataframe_method(name="count_it")
+        @register_geoseries_method(name="count_it")  # Support alias name also.
         @register_geodataframe_method
-        @register_geoseries_method
+        @register_geoseries_method  # Use accessor method `__name__` as the entrance.
         def counts(s: gpd.GeoSeries):
             # Counts the number of coordinate pairs in geometry
 
@@ -71,11 +80,26 @@ def counts(s: gpd.GeoSeries):
         1    1
         2    0
         Name: geometry, dtype: int64
+
+        In [7]: s.count_it()
+        Out[7]:
+        0    1
+        1    1
+        2    0
+        dtype: int64
+
+        In [8]: d.count_it()
+        Out[8]:
+        0    1
+        1    1
+        2    0
+        Name: geometry, dtype: int64
+
     """
-    return register_geoseries_accessor(method)
+    return register_geoseries_accessor(name)
 
 
 @register_method_factory
-@doc(register_geoseries_method, klass=":class:`geopandas.GeoDataFrame`")
-def register_geodataframe_method(method):
-    return register_geodataframe_accessor(method)
+@doc(register_geoseries_method, klass=":class:`~geopandas.GeoDataFrame`")
+def register_geodataframe_method(name: str | None = None):
+    return register_geodataframe_accessor(name)


Test patch:
diff --git a/test/accessor/test_register.py b/test/accessor/test_register.py
--- a/test/accessor/test_register.py
+++ b/test/accessor/test_register.py
@@ -1,4 +1,5 @@
 import pandas as pd
+import pytest
 
 from dtoolkit.accessor.register import register_dataframe_method
 from dtoolkit.accessor.register import register_series_method
@@ -17,14 +18,84 @@ def names(pd_obj):
     return pd_obj.columns.tolist()
 
 
+@register_dataframe_method()
+@register_series_method()
+def names_1(pd_obj):
+    """
+    An API to gather :attr:`~pandas.Series.name` and
+    :attr:`~pandas.DataFrame.columns` to one.
+    """
+    if isinstance(pd_obj, pd.Series):
+        return pd_obj.name
+
+    return pd_obj.columns.tolist()
+
+
+@register_dataframe_method(name="name_or_columns")
+@register_series_method(name="name_or_columns")
+def names_2(pd_obj):
+    """
+    An API to gather :attr:`~pandas.Series.name` and
+    :attr:`~pandas.DataFrame.columns` to one.
+    """
+    if isinstance(pd_obj, pd.Series):
+        return pd_obj.name
+
+    return pd_obj.columns.tolist()
+
+
 df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
 
 
-def test_method_hooked():
-    assert hasattr(df, "names")
-    assert hasattr(df.a, "names")
+@pytest.mark.parametrize(
+    "data, name",
+    [
+        (df, "names"),
+        (df.a, "names"),
+        (df, "names_1"),
+        (df.a, "names_1"),
+        (df, "name_or_columns"),
+        (df.a, "name_or_columns"),
+    ],
+)
+def test_method_hooked_exist(data, name):
+    assert hasattr(data, name)
+
+
+@pytest.mark.parametrize(
+    "data, name, excepted",
+    [
+        (df, "names", ["a", "b"]),
+        (df.a, "names", "a"),
+        (df, "names_1", ["a", "b"]),
+        (df.a, "names_1", "a"),
+        (df, "name_or_columns", ["a", "b"]),
+        (df.a, "name_or_columns", "a"),
+    ],
+)
+def test_work(data, name, excepted):
+    result = getattr(data, name)()
+
+    assert result == excepted
+
 
+@pytest.mark.parametrize(
+    "data, name, attr, excepted",
+    [
+        (df, "names", "__name__", names.__name__),
+        (df.a, "names", "__name__", names.__name__),
+        (df, "names", "__doc__", names.__doc__),
+        (df.a, "names", "__doc__", names.__doc__),
+        (df, "names_1", "__name__", names_1.__name__),
+        (df.a, "names_1", "__name__", names_1.__name__),
+        (df, "names_1", "__doc__", names_1.__doc__),
+        (df.a, "names_1", "__doc__", names_1.__doc__),
+        (df, "name_or_columns", "__doc__", names_2.__doc__),
+        (df.a, "name_or_columns", "__doc__", names_2.__doc__),
+    ],
+)
+def test_method_hooked_attr(data, name, attr, excepted):
+    method = getattr(data, name)
+    result = getattr(method, attr)
 
-def test_work():
-    assert df.names() == ["a", "b"]
-    assert df.a.names() == "a"
+    assert result == excepted
diff --git a/test/geoaccessor/test_register.py b/test/geoaccessor/test_register.py
--- a/test/geoaccessor/test_register.py
+++ b/test/geoaccessor/test_register.py
@@ -1,5 +1,6 @@
 import geopandas as gpd
 import pandas as pd
+import pytest
 from pygeos import count_coordinates
 from pygeos import from_shapely
 
@@ -17,16 +18,81 @@ def counts(s: gpd.GeoSeries):
     )
 
 
+@register_geodataframe_method
+@register_geoseries_method
+def counts_1(s: gpd.GeoSeries):
+    # Counts the number of coordinate pairs in geometry
+
+    return s.geometry.apply(
+        lambda x: count_coordinates(from_shapely(x)),
+    )
+
+
+@register_geodataframe_method(name="counts_it")
+@register_geoseries_method(name="counts_it")
+def counts_2(s: gpd.GeoSeries):
+    # Counts the number of coordinate pairs in geometry
+
+    return s.geometry.apply(
+        lambda x: count_coordinates(from_shapely(x)),
+    )
+
+
 s = gpd.GeoSeries.from_wkt(["POINT (0 0)", "POINT (1 1)", None])
-d = gpd.GeoDataFrame(geometry=s)
+d = s.to_frame("geometry")
+
+
+@pytest.mark.parametrize(
+    "data, attr",
+    [
+        (s, "counts"),
+        (d, "counts"),
+        (s, "counts_1"),
+        (d, "counts_1"),
+        (s, "counts_it"),
+        (d, "counts_it"),
+    ],
+)
+def test_method_hooked_exist(data, attr):
+    assert hasattr(data, attr)
+
+
+@pytest.mark.parametrize(
+    "data, name, excepted",
+    [
+        (s, "counts", pd.Series([1, 1, 0], name="geometry")),
+        (d, "counts", pd.Series([1, 1, 0], name="geometry")),
+        (s, "counts_1", pd.Series([1, 1, 0], name="geometry")),
+        (d, "counts_1", pd.Series([1, 1, 0], name="geometry")),
+        (s, "counts_it", pd.Series([1, 1, 0], name="geometry")),
+        (d, "counts_it", pd.Series([1, 1, 0], name="geometry")),
+    ],
+)
+def test_work(data, name, excepted):
+    result = getattr(data, name)()
 
+    assert result.equals(excepted)
 
-def test_method_hooked():
-    assert hasattr(s, "counts")
-    assert hasattr(d, "counts")
 
+@pytest.mark.parametrize(
+    "data, name, attr, excepted",
+    [
+        (s, "counts", "__name__", counts.__name__),
+        (s, "counts", "__doc__", counts.__doc__),
+        (d, "counts", "__name__", counts.__name__),
+        (d, "counts", "__doc__", counts.__doc__),
+        (s, "counts_1", "__name__", counts_1.__name__),
+        (s, "counts_1", "__doc__", counts_1.__doc__),
+        (d, "counts_1", "__name__", counts_1.__name__),
+        (d, "counts_1", "__doc__", counts_1.__doc__),
+        (s, "counts_it", "__name__", counts_2.__name__),
+        (s, "counts_it", "__doc__", counts_2.__doc__),
+        (d, "counts_it", "__name__", counts_2.__name__),
+        (d, "counts_it", "__doc__", counts_2.__doc__),
+    ],
+)
+def test_method_hooked_attr(data, name, attr, excepted):
+    method = getattr(data, name)
+    result = getattr(method, attr)
 
-def test_work():
-    result = pd.Series([1, 1, 0], name="geometry")
-    assert s.counts().equals(result)
-    assert d.counts().equals(result)
+    assert result == excepted