GithubHelp home page GithubHelp logo

aliasing's Introduction

aliasing

Tests Status Coverage Status Flake8 Status Code style: black

PyPi

aliasing is a small utility library for python which adds aliases to class members using python descriptors.

Usage

Direct use of @aliased

This is the recommended way to use the library if you are going to call the generated members in your scripts, as it supports IDE completion.

Basic usage involves a similar process to the builtin @property and @<property_name>.getter/setter. Instead we use @aliased to indicate the method whose register is being duplicated and @<aliased_name>.alias to indicate which method's name should be used to point to the "aliased" method:

from aliasing import aliased


class Example:
    @aliased
    def method(self):
        return "my method call"

    @method.alias
    def method_alias(self): ...

# elsewhere...

example = Example()
# note that `==` and `is` always returns False for function objects
# i.e., this is False:
#   example.method == example.method
assert example.method.__code__ is example.method_alias.__code__
assert example.method_alias() == "my method call"

Tip

To keep IDE completion for the methods marked with @<aliased_name>.alias the same as the method marked with @aliased, you must keep the signatures the same for now.

So:

class Example:
    @aliased
    def method(self, args: str) -> str: ...
    # This will NOT have the expected completion
    @method.aliased
    def method_alias1(self): ...
    # But this will
    @method.aliased
    def method_alias2(self, args: str) -> str: ...

I will investigate a solution for this, but it might not present itself.

Using "Virtual" Aliases to create a number of aliases at once

This is not strictly recommended for use in most cases unless your class will be processed by some framework which relies on the class members to generate something an end-user might want.

Basic usage involves decorating your class's methods with @valiases and specifying the names of however many aliases you want for that method.

For instance, if you want to make the function's old name available so as not to break the api, but remove it from IDE completion, you can add it as a virtual alias:

from aliasing import valiases

class Example:
    @valiases("my_func_old_name")
    def my_func(self):
        return "foo"

# elsewhere
example = Example()
# this is available with auto-complete:
foo = example.my_func()
# this will _work_ but will give the user an "Unresolved attribute" warning:
foo_old = example.my_func_old_name()

Warning

In the library's current state, it would be better to use other methods to mark the old function's name as deprecated, but in the future this library will hopefully be able to offer more support for this use-case.

This is a more convenient and shorter method of adding aliass to your

For example, in Google's python-fire cli tool, cli commands are "generated" using the members of a class, so you can easily add several commands at once using this library's @valiases decorator:

# fire_alias_example.py
import fire
from aliasing import valiases

class Example:
    @valiases("c", "cfg", "conf")
    def config(self):
        return "foo"

if __name__ == "__main__":
    fire.Fire(Example())

Then from cli, the user can call the aliased method or its aliases to achieve the same result:

$ python fire_alias_example.py config
foo
$ python fire_alias_example.py cfg
foo
$ python fire_alias_example.py conf
foo
$ python fire_alias_example.py c
foo

Or you might want to add names functions which do not follow PEP 8 naming conventions without disabling your linter or ide inspection settings. Perhaps this code will be called in another language, and you want to make sure the methods follow that language's style as well:

from aliasing import valiases

class Example:
    @valiases("MyFunc", "myFunc", "MY_FUNC")
    def my_func(self):
        return "foo"

Advanced Usage

alias Descriptor

Since alias objects are just python descriptors (docs), you can use them with more granularity too:

from aliasing import alias


class Example:
    my_alias = alias(alias_for="prop")
    
    def __init__(self, val):
        self.prop = val

# elsewhere
example = Example(object())
assert example.prop is example.my_alias

You can define them independently from classes then attach them to any number of classes without hierarchical relation.

class Foo:
    def __init__(self, val):
        self.prop = val
        
class Bar:
    def __init__(self, val):
        self._prop = val
        
    @property
    def prop(self):
        return f"Bar.prop: {self._prop}"

# elsewhere
from aliasing import alias

prop_alias = alias(alias_for="prop", alias_name="my_alias")

prop_alias.attach(Foo)
prop_alias.attach(Bar)

# now both classes have the alias named "my_alias" pointing to "prop"
assert Foo('baz').my_alias == 'baz'
assert Bar('baz').my_alias == 'Bar.prop: baz'

You can check out the tests to see some more examples of alternative ways to attach alaiss to your classes.

aliased Descriptor

You can also initialize aliased descriptors independently from classes:

from aliasing import aliased

trample_ok Parameters

By default, the aliasing library will raise a TrampleAliasError if you try to override existing class attributes or members without specifying trample_ok for that alias.

For instance, this will fail when alias.attach is called:

class Foo:
    def __init__(self, val):
        self.prop = val
    
    @staticmethod
    def my_alias():
        return "don't trample me"

# elsewhere
from aliasing import alias

prop_alias = alias(alias_for="prop", alias_name="my_alias")

# fails with TrampleAliasError("Owner calls Foo already has member with name my_alias. [..]")
prop_alias.attach(Foo)

And this will fail whenever the class is imported:

from aliasing import valiases

class Foo:
    # fails with TrampleAliasError("Owner class Foo already has member with name __str__. [..]")
    @valiases("__str__")
    def to_str(self): ...

But you can pass trample_ok in a couple different ways to override this behavior.

For alias:

class Foo:
    def __init__(self, val):
        self.prop = val

    @staticmethod
    def my_alias():
        return "don't trample me"

# elsewhere
from aliasing import alias

prop_alias = alias(alias_for="prop", alias_name="my_alias")

# trample_ok causes a warning TrampleAliasWarning, but no longer fails
prop_alias.attach(Foo, trample_ok=True)

assert Foo('change is good').my_alias == 'change is good'

# OR if you prefer you can set `trample_ok` on the alias itself:
prop_alias = alias(alias_for="prop", alias_name="new_alias", trample_ok=True)
prop_alias.attach(Foo)
assert Foo('this also works').new_alias == 'this also works'

For valiases:

from aliasing import valiases

class Foo:
    # trample_ok causes a warning TrampleAliasWarning, but no longer fails
    @valiases("__str__", trample_ok=['__str__'])
    def to_str(self):
        return 'new __str__ for Foo'

# I would not recommend 'trampling' magic methods like __str__
# ... but it's your life
assert Foo().__str__() == 'new __str__ for Foo'

The major benefit of this is that your can easily override several methods at once if they all do the same thing (like return "NotImplemented"):

import abc
from typing import Any

class CrudBase(abc.ABC):
    @abc.abstractmethod
    def create(self, name: str, data: Any) -> Any: ...

    @abc.abstractmethod
    def read(self, name: str) -> Any: ...

    @abc.abstractmethod
    def update(self, name: str, partial_data: Any) -> Any: ...

    @abc.abstractmethod
    def delete(self, name: str) -> Any: ...

# elsewhere
from aliasing import valiases
from some_persistence_lib import read_method

class ReadOnlyBase(CrudBase, abc.ABC):
    options = {'arg': 'val'}
    
    # now if any of these methods are called, they will get 'NotImplemented'
    # this saves you from redefining the same 2-liner method 3 times.
    # obviously the benefits are better the more methods there are to override
    @valiases('create', 'update', 'delete', trample_ok=['create', 'update', 'delete'])
    def _not_implemented(self, *args, **kwargs) -> Any:
        return NotImplemented
    
    def read(self, name: str) -> Any:
        return read_method(name, **self.options)

Questions, Contributing, Feature requests

If you'd like to get in touch for any reason, the easiest thing is opening a GitHub issue.

Please give older issues (including closed!) a look before opening anything and I'll try to respond whenever I can.

Note

About feature requests, the plan for this library is to keep it extremely small. In its present state, I think it has a bit of room to grow, but it is designed with 1 thing in mind and 1 thing only: duplicating members of classes under different names.

I'm more than happy to accept any and all feature requests that keep with this theme, but I reserve the right to deny them for any reason. I'll keep it reasonable, and will always be respectful.

If there is a request for something that's a little outside the scope of the project but maybe is related enough, I'll consider adding an 'extensions' library.

aliasing's People

Contributors

mxndtaylor avatar

Stargazers

 avatar

Watchers

 avatar

aliasing's Issues

Feature: allow calling `attach` on an instance of a class without modifying the class itself

Is your feature request related to a problem? Please describe.
It's a little frustrating that if you call alias.attach on an instance variable, the object's class will be modified with the alias.
For instance:

import aliasing as aka

class Foo:
    def method(self):
        return "baz"

bar = aka.alias(alias_for="method", alias_name="bar")
f1 = Foo()

bar.attach(f1)
# prints "True"
print(f1.bar() == f1.method())

f2 = Foo()
# prints "True"
print(f2.bar() == f1.method())

Describe the solution you'd like
I'd like to see this behavior instead:

import aliasing as aka

class Foo:
    def method(self):
        return "baz"

bar = aka.alias(alias_for="method", alias_name="bar")
f1 = Foo()

bar.attach(f1)
# prints "True"
print(f1.bar() == f1.method())

f2 = Foo()
# raises AttributeError
print(f2.bar() == f1.method())

Feature: method to automatically generate alias names adds aliases based on pre-defined strategies

Describe the solution you'd like

a decorator (something like @auto_alias) that accepts string options, enum flags or boolean parameters (or some combination)
that generates aliases, so make these use cases easier:

  • "short", alternatively "sub", which generates sub stringed aliases so aardvark would generate a, aa, aar, aard, etc. could accept an Sequence of ints representing indexs to terminate on, something like short=[1, 5] for aardvark would generate a and aardv.
  • "acronym", acronymizes the member get_my_data_please is given alias gmdp
  • "un_private", removes a single leading underscore if there is only 1 leading, removes any number at the end
  • "un_mangled", removes two or more leading underscores and at most 1 at the end
  • "un_magic", removes all leading and all tailing underscores if two or more exist on both ends
  • "case", accepts strategies for naming conventions
    • camel, pascal, caps, kebab, title, sentence, snake, macro, flat, upper, cobol
    • "insensitive" or "all" or something that generates all of them
    • for now, assumes snake_case, but I'll try see about detecting the type of case
    • accepts the parameters autologically? "camelCase" "PascalCase" "kebab-case" etc.

Describe alternatives you've considered

@valiases with the exact parameters specified but this could get out of hand and turn into a lot of work to support the same use cases all at the same time. It's also static rather than dynamic. I'd need to define it per member based on it's name. The above solution would automatically change to the new aliases based on the new method name if it was ever changed

Feature: module-level aliases

Is your feature request related to a problem?

alias and aliased are limited to use inside classes due them being descriptors

Describe the solution you'd like

I'd like to see either another decorator or descriptor that can be used at the module level.

Solutions that I see at the moment, none of which seem ideal:

  • a dummy class that extends module and holds the aliases
    • problem is, when a module is imported, it would not return the dummy class
  • a decorator that actually copies by value instead of reference
    • the current alias implementation only adds a new attribute to a class which references the same thing

Describe alternatives you've considered

I've tried using the existing descriptors and they do not work, I cannot attach the alias to an already existing module
due to them being immutable, and __set_name__ is not called when assigning descriptors outside of a class.

__get__ is also not called when accessing a module's attribute that defines it.

Also... can just do:

def method():
    return 'test'

my_alias = method

Feature: alias names with starting with `__` should be mangled when attached to a class

Is your feature request related to a problem?

python's mangling system is convenient, but with this method of aliasing member, we do not mangle the alias names.

leading to issues like this:

class Foo:
    @aka.valiases("__mangled_alias")
    def __mangled_initial(self):
        return "val"

print(Foo().__mangled_alias())
# prints "val"

I would expect the above to fail, and leave the alias's name as _Foo__mangled_alias instead, so that this succeeds:

class Foo:
    @aka.valiases("__mangled_alias")
    def __mangled_initial(self):
        return "val"

print(Foo()._Foo__mangled_alias())
# prints "val"

Describe the solution you'd like

if alias.__set_name__ gets a name that has leading __ (with at most 1 trailing _), then mangle the alias's name based on owner.__name__.

ex:

class Foo:
    prop = 1

prop_alias = alias(alias_name="__prop")
prop_alias.attach(Foo)
assert Foo()._Foo__prop == 1

Additional context

We might want to add a parameter no_mangle or such that disables this behavior

Feature: some way of getting properties of properties like `@alias("primary.nested")` or something

Is your feature request related to a problem? Please describe.
aliasing currently is only grabbing values "flatly" in the sense that it only gets properties of the immediate class it's attached to.

Describe the solution you'd like
a descriptor that repeats the de-aliasing process on it's result based on a series of terms.
some like accepting varargs like *props: str or a path variable with delimiters like "prop.prop"

Describe alternatives you've considered
jsonpath_ng or jq do similar things, but they involve more processing, I just want something extremely simple and straightforward.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.