Giter Site home page Giter Site logo

dmytrostriletskyi / accessify Goto Github PK

View Code? Open in Web Editor NEW
109.0 2.0 7.0 41 KB

Python design kit: interfaces, declared exception throws, class members accessibility levels (private and protected methods for humans).

License: MIT License

Python 100.00%
interfaces access-modifiers design-kit design-tool python3

accessify's Introduction

accessify

Release PyPI version shields.io Build Status codecov

Downloads PyPI license PyPI pyversions

Habrahabr

Getting started

What is accessify

accessify is a Python design kit that provides:

  • interfaces,
  • declared exceptions throws,
  • class members accessibility levels.

that could be combined with each other to make your code slim and this library usage more justified.

Access modifiers

Access level modifiers determine whether other classes can use a particular field or invoke a particular method. Accessibility levels are presented from the box in the languages like C++, C# and Java.

class Car 
{
    private string StartEngine()
    {
        // Code here.
    }
}

But Python does not have this in the same way.

Motivation

  • We're all consenting adults here that is the part of the Python philosophy that relies on human factor instead of the interpreter.
  • There is a Python convention that is to use an underscore prefix for protected and private members, that is a bit ugly. Isn't it? For instance, for the following piece of code that provides class a private member.
class Car:

    def __start_engine(self, *args, **kwargs):
        pass
  • Moreover, private and protected methods could be easily accessed outside the class. This is really a point to postpone the correct design of the system to the backlog, increasing the technical debt.
class Car:

   def _start_engine(self, *args, **kwargs):
       pass
       
   def __start_engine(self, *args, **kwargs):
       pass


car = Car()
car._start_engine()
car._Car__start_engine()

Interfaces

An interface is a contract specifying a set of methods and properties which required to be available on any implementing class. If the class implements an interface, but does not realize its method, corresponding errors should be raised. Interfaces are presented from the box in the languages like C++, C# and Java.

interface HumanInterface
{
    public string EatFood();
}

class Human : HumanInterface
{
    public string EatFood()
    {
        // Code here.
    }
}

But Python does not have this in the same way.

Motivation

  • The interface makes checks during the implementation creation, but not actually while execution like abc module in Python.
  • The interface requires that implementation's method arguments match with arguments declared in interfaces, abc — not.
  • A lot of libraries that provide interfaces are no longer supported.
  • A lot of libraries that provide interfaces require you to write a lot of code to use its functionality, this library — not.

How to install

Using pip install the package from the PyPi.

$ pip3 install accessify

Usage

Access modifiers

Private

  • Private members are accessible only within the body of the class.

In this example, the Car class contains a private member named start_engine. As a private member, they cannot be accessed except by member methods. The private member start_engine is accessed only by way of a public method called run.

from accessify import private


class Car:

    @private
    def start_engine(self):
        return 'Engine sound.'

    def run(self):
        return self.start_engine()


if __name__ == '__main__':
    car = Car()

    assert 'Engine sound.' == car.run()

    car.start_engine()

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/access/private.py", line 24, in <module>
    car.start_engine()
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/main.py", line 92, in private_wrapper
    class_name=instance_class.__name__, method_name=method.__name__,
accessify.errors.InaccessibleDueToItsProtectionLevelException: Car.start_engine() is inaccessible due to its protection level

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhASP > private.py
$ python3 private.py
  • Child classes cannot access parent private members.

In this example, the Car class contains a private member named start_engine. As a private member, they cannot be accessed from the child classes, Tesla in our case. So overridden method run by Tesla class cannot use the parent's start_engine member.

from accessify import private


class Car:

    @private
    def start_engine(self):
        return 'Engine sound.'


class Tesla(Car):

    def run(self):
        return self.start_engine()


if __name__ == '__main__':
    tesla = Tesla()
    tesla.run()

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/inheritance/private.py", line 23, in <module>
    tesla.run()
  File "examples/inheritance/private.py", line 18, in run
    return self.start_engine()
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/main.py", line 94, in private_wrapper
    class_name=class_contain.__name__, method_name=method.__name__,
accessify.errors.InaccessibleDueToItsProtectionLevelException: Car.start_engine() is inaccessible due to its protection level

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhASX > inheritence_private.py
$ python3 inheritence_private.py

Protected

  • A protected member is accessible within its class and by derived class instances.

In this example, the Car class contains a protected member named start_engine. As a protected member, they cannot be accessed except by member methods. The protected member start_engine is accessed only by way of a public method called run.

from accessify import protected


class Car:

    @protected
    def start_engine(self):
        return 'Engine sound.'

    def run(self):
        return self.start_engine()


if __name__ == '__main__':
    car = Car()

    assert 'Engine sound.' == car.run()

    car.start_engine()

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/access/protected.py", line 21, in <module>
    car.start_engine()
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/main.py", line 134, in protected_wrapper
    class_name=instance_class.__name__, method_name=method.__name__,
accessify.errors.InaccessibleDueToItsProtectionLevelException: Car.start_engine() is inaccessible due to its protection level

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhASM > protected.py
$ python3 protected.py
  • Child classes have access to those protected members.

In this example, the Car class contains a protected member named start_engine. As a protected member, they can be accessed from the child classes, Tesla in our case. So overridden method run by Tesla class can use the parent's start_engine member.

from accessify import protected


class Car:

    @protected
    def start_engine(self):
        return 'Engine sound.'


class Tesla(Car):

    def run(self):
        return self.start_engine()


if __name__ == '__main__':
    tesla = Tesla()

    assert 'Engine sound.' == tesla.run()

The code will work without errors.

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhASD > inheritence_protected.py
$ python3 inheritence_protected.py

Other features

  • The accessify decorator removes private and protected members from class dir.
from accessify import accessify, private


@accessify
class Car:

    @private
    def start_engine(self):
        return 'Engine sound.'

if __name__ == '__main__':
    car = Car()

    assert 'start_engine' not in dir(car)

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhASy > dir.py
$ python3 dir.py

Interfaces

Single interface

  • When you declare that class implements an interface, a class should implement all methods presented in the interface.

In this example, there is an interface called HumanInterface that contains two methods love and eat. Also, there is a class Human that implements the interface but missed method «eat», so the corresponding error should be raised.

from accessify import implements


class HumanInterface:

    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        pass

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/interfaces/single.py", line 18, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 66, in decorator
    interface_method_arguments=interface_method.arguments_as_string,
accessify.errors.InterfaceMemberHasNotBeenImplementedException: class Human does not implement interface member HumanInterface.eat(food, args, allergy, kwargs)

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhh2V > single_method.py
$ python3 single_method.py
  • When you declare that class implements an interface, a class should implement all methods that presented in the interface including number, order and naming of the accepting arguments.

In this example, there is an interface called HumanInterface that contains two methods love and eat. Also, there is a class Human that implements the interface but missed 3 of 4 arguments for method «eat», so the corresponding error should be raised.

from accessify import implements


class HumanInterface:

    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        @staticmethod
        def eat(food):
            pass

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/interfaces/single_arguments.py", line 16, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 87, in decorator
    interface_method_arguments=interface_method.arguments_as_string,
accessify.errors.InterfaceMemberHasNotBeenImplementedWithMismatchedArgumentsException: class Human implements interface member HumanInterface.eat(food, args, allergy, kwargs) with mismatched arguments

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhh2w > single_arguments.py
$ python3 single_arguments.py
  • When you declare that class implements an interface, a class should implement all methods that presented in the interface including number, order and naming of the accepting arguments and access modifier type.

In this example, there is an interface called HumanInterface that contains two methods love and eat. Also, there is a class Human that implements the interface but missed private access modifier type for method «eat», so the corresponding error should be raised.

from accessify import implements, private


class HumanInterface:

    @private
    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        @staticmethod
        def eat(food, *args, allergy=None, **kwargs):
            pass

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/interfaces/single_access.py", line 18, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 77, in decorator
    interface_method_name=interface_method.name,
accessify.errors.ImplementedInterfaceMemberHasIncorrectAccessModifierException: Human.eat(food, args, allergy, kwargs) mismatches HumanInterface.eat() member access modifier.

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhh2r > single_access.py
$ python3 single_access.py

Multiple interfaces

  • A class could implement multiple interfaces.
  • When you declare that class that implements a bunch of interfaces, a class should implement all method that presented in each interface including number, order and naming of the accepting arguments and access modifier type.

In this example, there are an interface HumanSoulInterface that contains a method called love and interface HumanBasicsInterface that contains a method called eat. Also, there is a class Human that implements method love from the first interface, but missed method «eat» from the second one, so the corresponding error should be raised.

from accessify import implements


class HumanSoulInterface:

    def love(self, who, *args, **kwargs):
        pass


class HumanBasicsInterface:

    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanSoulInterface, HumanBasicsInterface)
    class Human:

        def love(self, who, *args, **kwargs):
            pass

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/interfaces/multiple.py", line 19, in <module>
    @implements(HumanSoulInterface, HumanBasicsInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 66, in decorator
    interface_method_arguments=interface_method.arguments_as_string,
accessify.errors.InterfaceMemberHasNotBeenImplementedException: class Human does not implement interface member HumanBasicsInterface.eat(food, args, allergy, kwargs)

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhh2o > multiple.py
$ python3 multiple.py

Exception throws declaration

  • When you declare that interface method throws a particular exception, a class method that implement interface should contain code in the body that raise this exception.
  • You can declare that the interface method throws multiple exceptions.

In this example, exception HumanDoesNotExistsError and exception HumanAlreadyInLoveError are declared to be raised by the Human class method called love , but method missed to raise the second exception, so the corresponding error should be raised.

from accessify import implements, throws


class HumanDoesNotExistsError(Exception):
    pass


class HumanAlreadyInLoveError(Exception):
    pass


class HumanInterface:

    @throws(HumanDoesNotExistsError, HumanAlreadyInLoveError)
    def love(self, who, *args, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        def love(self, who, *args, **kwargs):

            if who is None:
                raise HumanDoesNotExistsError('Human whom need to love does not exist.')

The code above will produce the following traceback.

Traceback (most recent call last):
  File "examples/interfaces/throws.py", line 21, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 103, in decorator
    class_method_arguments=class_member.arguments_as_string,
accessify.errors.DeclaredInterfaceExceptionHasNotBeenImplementedException: Declared exception HumanAlreadyInLoveError by HumanInterface.love() member has not been implemented by Human.love(self, who, args, kwargs)

Test it out using the examples. Get the example that contains the code above by curl and run it by python3.

$ curl -L https://git.io/fhh26 > throws.py
$ python3 throws.py

Disable checking

You can disable all accessify checks. For instance, in the production, when you shouldn't check it because it already was checked in the development. Use the following environment variable then:

export DISABLE_ACCESSIFY=True

Contributing

Clone the project and install requirements:

$ git clone [email protected]:dmytrostriletskyi/accessify.git && cd accessify
$ pip3 install -r requirements-dev.txt
$ pip3 install -r requirements-tests.txt

If you prefer working with the Docker and wanna easily change Python environments, follow:

$ git clone [email protected]:dmytrostriletskyi/accessify.git && cd accessify
$ export ACCESSIFY_PYTHON_VERSION=3.4
$ docker build --build-arg ACCESSIFY_PYTHON_VERSION=$ACCESSIFY_PYTHON_VERSION -t accessify . -f Dockerfile-python3.x
$ docker run -v $PWD:/accessify --name accessify accessify

Enter the container bash, check Python version and run tests:

$ docker exec -it accessify bash
$ root@36a8978cf100:/accessify# python --version
$ root@36a8978cf100:/accessify# pytest -vv tests

Clean container and images with the following command:

$ docker rm $(docker ps -a -q) -f
$ docker rmi $(docker images -q) -f

When you will make changes, ensure your code pass the checkers and is covered by tests using pytest.

If you are new for the contribution, please read:

References

Check it out to familiarize yourself with class members accessibility levels:

accessify's People

Contributors

dependabot[bot] avatar dmytrostriletskyi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

accessify's Issues

Can't use parent's public method with protected method in it.

Hello!
Here my code that raises some errors that I can't understand.

class Car:
    @protected
    def start_engine(self):
        return "Engine's sound."

    def run(self):
        return self.start_engine()


class Tesla(Car):
    pass


if __name__ == "__main__":
    t = Tesla()
    t.run()

This code raises error

Traceback (most recent call last):
  File "/home/mostepan/dev/food/plgr.py", line 98, in <module>
    t.run()
  File "/home/mostepan/dev/food/plgr.py", line 89, in run
    return self.start_engine()
  File "/home/mostepan/dev/food/venv/lib/python3.10/site-packages/accessify/access.py", line 112, in protected_wrapper
    raise InaccessibleDueToItsProtectionLevelException(
accessify.errors.InaccessibleDueToItsProtectionLevelException: Tesla.start_engine() is inaccessible due to its protection level

I think there is an error somewhere.

[Bug] self literal issue

This lib becomes quite fragile when it comes to self literal. Check out the example below (which is a slightly modified example from the readme):

from accessify import private


class Car:

    @private
    def start_engine(self):
        return 'Engine sound.'

    def run(self):
        return self.start_engine()


class Hooman:

    def drive(another_self):
        self = Car()

        assert 'Engine sound.' == self.run()

        print(self.start_engine())


if __name__ == '__main__':
    hooman = Hooman()
    hooman.drive()

When you execute this code you will not get any exceptions. I assume this happens because Car() object is assigned to a variable called self. Which brings me to an idea that this is not how it should be implemented:

class ClassMemberDefaultArguments:
    """
    Provide class members arguments default values.
    """

    SELF = 'self'
    CLS = 'cls'

Otherwise, you are just relying on another Python convention (about the usage of the self literal), which you tried to avoid in the first place.

Too large exceptions names

The exceptions names are too large, it is more descriptive but also makes harder to write them, please make a balance between both things

Private/protected functions inaccessible through list comprehensions

Calls to functions marked as private/protected throw an error when inside a list comprehension. Example:

class Test:
    def a(self):
        return [self.b(x) for x in "12345"]

    @private
    def b(self, x):
        return f"{x}."

 
t = Test()
t.a()

Error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in a
  File "<stdin>", line 3, in <listcomp>
  File "/.venvs/test/lib/python3.10/site-packages/accessify/access.py", line 75, in private_wrapper
    raise InaccessibleDueToItsProtectionLevelException(
accessify.errors.InaccessibleDueToItsProtectionLevelException: Test.b() is inaccessible due to its protection level

In accessify, it seems to be raised here:

# access.py
            ...
            method_caller_frame = inspect.currentframe().f_back
            method_caller_class = get_method_class_by_frame(frame=method_caller_frame)

            if instance_class is not method_caller_class:
                raise InaccessibleDueToItsProtectionLevelException(
                    INACCESSIBLE_DUE_TO_ITS_PROTECTION_LEVEL_EXCEPTION_MESSAGE.format(
                        class_name=instance_class.__name__, class_method_name=method.__name__,
                    ),
                )

Removing the private/protected decorator makes the error go away, as does rewriting the list comp to a for-loop assignment. Perhaps related to Python 3's scoping for list comprehensions - can there be a workaround?

Tested on Python 3.10.6

MyClass.method() is inaccessible due to its protection level

Hi. It's a great library, and I'm happy to write methods without ugly underscores.

I've been using the decorators for a couple of days, and today I run into an error that, in my opinion, shouldn't happen.

Basically, I'm calling a protected method from a public method. And I'm getting:

File "/home/guillermo/.pyenv/versions/3.6.8/lib/python3.6/site-packages/accessify/access.py", line 114, in protected_wrapper
class_name=instance_class.name, class_method_name=method.name,
accessify.errors.InaccessibleDueToItsProtectionLevelException: FirstStrategy.sell() is inaccessible due to its protection level

So, FirstStrategy.sell() is inaccessible due to its protection level

This is a simplified version of the class:

from abc import ABC, abstractmethod

from accessify import protected

class Strategy(ABC):
    def __init__(self):
        super().__init__()

    def sell_all(self, share_prices):
        for share_price in share_prices:
            self.sell(share_price)  # this is the line causing trouble

    @protected
    def sell(self, share_price: SharePrice):
        ticker = share_price.ticker
        quantity = self.shares.get_quantity_for(ticker)
        if quantity == 0:
            return None

        profit = self.shares.sell(ticker, share_price)
        self.cash.deposit(profit)

        operation = create_sell_operation(share_price, quantity)

        return operation

And here's the actual class:

from abc import ABC, abstractmethod
from typing import List

from accessify import protected

from simulator.entities.cash import Cash
from simulator.entities.operation import create_buy_operation, create_sell_operation
from simulator.entities.share_price import SharePrice
from simulator.entities.shares import Shares


class Strategy(ABC):
    def __init__(self,
                 cash: Cash = Cash(0),
                 investment_per_operation=0,
                 shares: Shares = Shares()):
        self.investment_per_operation = investment_per_operation
        self.shares = shares
        self.cash = cash
        super().__init__()

    @abstractmethod
    def operate_with(self, share_price: SharePrice, trace):
        pass

    @abstractmethod
    def should_buy(self, share_price: SharePrice, previous_prices: List[float]):
        pass

    @abstractmethod
    def should_sell(self, share_price: SharePrice, previous_prices: List[float]):
        pass

    def sell_all(self, share_prices):
        for share_price in share_prices:
            self.sell(share_price)

    @protected
    def invest(self, share_price: SharePrice):
        available_money = self.cash.withdraw(self.investment_per_operation)
        quantity = share_price.calculate_shares_amount_for(available_money)
        if quantity == 0:
            return None

        operation = create_buy_operation(share_price, quantity)
        self.shares.add(share_price.ticker, quantity)

        return operation

    @protected
    def sell(self, share_price: SharePrice):
        ticker = share_price.ticker
        quantity = self.shares.get_quantity_for(ticker)
        if quantity == 0:
            return None

        profit = self.shares.sell(ticker, share_price)
        self.cash.deposit(profit)

        operation = create_sell_operation(share_price, quantity)

        return operation

    def __lt__(self, other):
        return False

Any clue on why is this happening?

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.