Coder Social home page Coder Social logo

dry-python / classes Goto Github PK

View Code? Open in Web Editor NEW
650.0 16.0 23.0 1.08 MB

Smart, pythonic, ad-hoc, typed polymorphism for Python

Home Page: https://classes.rtfd.io

License: BSD 2-Clause "Simplified" License

Python 100.00%
python python3 mypy mypy-stubs mypy-plugins pep561 fp typesafety typeclasses

classes's People

Contributors

antonagestam avatar dependabot-preview[bot] avatar dependabot[bot] avatar ftaebi avatar github-actions[bot] avatar mikhail-akimov avatar q0w avatar shalokshalom avatar sobolevn avatar ssyrota avatar thepabloaguilar avatar vpoulailleau 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

classes's Issues

Dependabot can't resolve your Python dependency files

Dependabot can't resolve your Python dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Creating virtualenv classes-Cg1ia3QJ-py3.9 in /home/dependabot/.cache/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies...

  PackageNotFound

  Package sphinx-typlog-theme (0.8.0) not found.

  at /usr/local/.pyenv/versions/3.9.0/lib/python3.9/site-packages/poetry/repositories/pool.py:144 in package
      140│                     self._packages.append(package)
      141│ 
      142│                     return package
      143│ 
    → 144│         raise PackageNotFound("Package {} ({}) not found.".format(name, version))
      145│ 
      146│     def find_packages(
      147│         self, dependency,
      148│     ):

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Multiple dispatcher

Hi @sobolevn

It would be great to do something with an interface like this

@impl
def add(a: int, b: int) -> int:
    return a + b

@impl
def add(a: str, b: str) -> str:
    return '{0}{1}'.format(a, b)

If it's not a good idea to parse in runtime type annotations

@impl(int, int)
def add(a: int, int) -> int:
    return a + b

@impl(str, str)
def add(a: str, b: str) -> str:
    return '{0}{1}'.format(a, b)

What do you think?

Merge with singledispatch?

Hi, love the project, I've definitely wanted better mypy support in singledispatch before, as well as other more advanced features.

That being said, it looks like mypy is going to add support for a singledispatch plugin. Maybe there's some opportunity for collaboration?
mypyc/mypyc#802

Dependabot can't resolve your Python dependency files

Dependabot can't resolve your Python dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Creating virtualenv classes-vz-MTj5d-py3.8 in /home/dependabot/.cache/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies...

[PackageNotFound]
Package wemake-python-styleguide (0.13.0) not found.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Enable `Supports[]` to refer to multiple typeclasses

class Foo:
    def __call__(self, instance) -> A:
        ...

foo = typeclass(Foo)

...

class Bar:
    def __call__(self, instance) -> B:
        ...

bar = typeclass(Bar)

...

def fn(instance: Supports[Foo, Bar]) -> C:
    ...

This is similar to #83 and python/typing#213, but offers the advantages of composability and conciseness.

I'd be willing to have a go at implementing this but I'm not very familiar with mypy plugins (where I suspect most of the work would be required).

Revert "get type from function annotation"

Ok, I really thought that this is a good idea. But, now it seems to me that I was wrong.

Why?

  1. First of all, it is very limited. For example, you cannot write def a(instance: Union[str, bytes]). We can only work with Instance types because of the limited runtime-dispatch capabilities
  2. It makes our code much more complex. And I mean it! With this feature we now have to typecheck a whole other set of cases. Let's see some examples:
def ex():
   ...

some.instance(ex)  # will typecheck, because we allow callables, but should not.
# So, we need a plugin support for this - and it is really complex, because it has lots of corner cases
  1. It makes our code much less type-safe. There are cases when Callable should not be allowed into first .instance() call
  2. Runtime algorithm is not perfect as well, it is now focused on getattr(arg, '__annotations__') and the thing is arg might have __annotations__ even if it is not a function. So, I need to figure out another solution. But, probably the best way to solve this is to remove this feature

So, I have decided to remove this. Yes, it is a breaking changed, but there are just several users, and upgrade is really easy.

Reverts #136

Hypothesis integration

Right now we don't test classes with hypothesis at all.

But, I have a great idea (at least it seems to me like so) of adding a hypothesis plugin, similar to one we have in returns: https://github.com/dry-python/returns/blob/master/returns/contrib/hypothesis/laws.py

What should it do?

  • check_typeclass(typeclass_instance) will check that for all existing types and protocols some valid return type is produced. For example, @typeclass def some(instance) -> str must produce str for all instance, protocol, and supported types and it must produce NotImplementedError for types that are not supported
  • Looks like check_typeclass must have allow_instances, allow_protocols, allow_unknown kw bool arguments to configure what types we actually need in a resulting strategy

It would be a nice companion to our mypy check.

@Zac-HD do you like it? Maybe you have any other ideas?

Concrete generics support

Right now it is impossible to write a code like this one:

from classes import typeclass
from typing import List

@typeclass
def some(instance) -> int:
    ...

@some.instance(List[int])
def _some_list_int(instance: List[int]) -> int:
    ...

Because, you cannot check that some variable has type List[int] in runtime.

Maybe we can use runtime typecheckers? Like: https://github.com/erezsh/runtype

Related #8

I cannot register `None`

This code does not work:

@typeclass
def some(instance) -> str:
    ...

@some.instance(None)
def _some_none(instance: None) -> str:
    ...

But, it should. Otherwise, we won't have any way to register None handler.

Btw, type(None) does not work as well.

Dependabot couldn't find a Pipfile for this project

Dependabot couldn't find a Pipfile for this project.

Dependabot requires a Pipfile to evaluate your project's current Python dependencies. It had expected to find one at the path: /docs/requirements.txt‎/Pipfile.

If this isn't a Python project, or if it is a library, you may wish to disable updates for it from within Dependabot.

View the update logs.

Check that subclasses of `AssociatedType`s do not have their own bodies

Right now a person can write something like this:

class MyClass(AssociatedType):
    x = 1

It does not make any sence, because MyClass is only used for typing. Moreover, it can cause a lot of confusion and bad usage practices.

So, we need to validate that MyClass is not used incorrectly.

Use `returns` in testing

We need to be sure that classes and returns can be used together and that typing works.

Plan:

  1. Install returns as dev-dependency
  2. Create special typesafety/test_integration/test_returns folder where we would test it
  3. In pytest-mypy-plugins we would need to add returns_plugin to mypy settings

How to implement conversion where return type is polymorphic?

I am trying to implement a function which would deserialize arbitrary values from string representations. For example, I'd like to say

class Person(pydantic.BaseModel):
    name: str
    job: str

>>> from_json_string(Person, '{"name": "John Doe", "job": "baker"}')
Person(name='John Doe', job='baker')

Of course that requires an implementation for from_json_string, something like this:

T = TypeVar('T')

@from_json_string.instance(pydantic.BaseModel)
def pydantic_from_json_string(model_class: Type[T], raw_value: str) -> T:
    return model_class(**json.loads(raw_value))

I do not seem to be able to do this of course, since the library believes I am looking for instances of BaseModel whereas I am targeting the class itself.

@from_json_string.instance(Type[pydantic.BaseModel]) does not work because it does not match on generics.

Any workarounds?

raise mypy errors when type is added more than once

This code:

@typeclass
def test(instance) -> str:
    ...

@test.instance(str)
def _test_str(instance) -> str:
    ...

@test.instance(str)  # error here!
def _test_str2(instance) -> str:
    ...

Should raise an error during type checking. Possibly also during runtime

Strict `__call__` invariance

Now we have a problem, because function args in Python are covariant we can use it incorrectly with a typeclass.

Let's see an example:

from classes import typeclass

class A:
    ...

class B(A):
    ...

class C(B):
    ...

@typeclass
def some(instance) -> str:
    ...

@some.instance(B)
def _some_b(instance: B) -> str:
    ...

reveal_type(some(A()))  # Should be an error
# Argument 1 to "some" has incompatible type "A"; expected "B"

reveal_type(some(B()))  # ok

reveal_type(some(C()))  # Should be an error, but is not

Typeclasses with several methods

There's a common use-case when we need to create a type-class with several methods. Like:

class Functor(Protocol[T]):
    def map(self, function: Callable[[T], Functor[V]]) -> Functor[V]:
        ...

    @classmethod
    def from_value(cls, inner_value: V) -> Functor[V]:
        ...

Currently, there's no way we can do that.

I propose something like this:

@typeclass
class Functor(Protocol[T]):
    def map(self, function: Callable[[T], Functor[V]]) -> Functor[V]:
        ...

    @classmethod
    def from_value(cls, inner_value: V) -> Functor[V]:
        ...

I am not sure how to bound it to the actual implementation.

What to do with duplicates?

For example:

@typeclass
def some(instance): ...

@some.instance(int):
def _some_int1(instance: int): ...

@some.instance(int):
def _some_int2(instance: int): ...

There are several things we can do:

  1. Allow this, in this case we need to provide a test case and docs about when this can be useful
  2. Add options, like @typeclass(final=True), it won't allow exact duplicates when activated, but by default we will allow it
  3. Always forbid, but in this case people might end up with a situation when they need to override an existing behaviour but can't

Consider adding generics support

We need to be able to work with generic like List[str] or Result[str, Exception] and Maybe[int]

I propose the following API:

@typeclass
def test(instance) -> int:
    ...

@test.instance(
    Some[int], 
    predicate=lambda instance: is_successful(instance) and isinstance(instance.unwrap(), int),
)
def _test_success_int(instance: Some[int]) -> int:
     return instance.unwrap()

It should be supported on both side: types and runtime.

Mypy crash on nested typeclasses

This code:

from classes import typeclass

def some():
    @typeclass
    def a(instance) -> str:
        ...

    @a.instance(int)
    def _a_int(instance: int) -> str:
        ...

    reveal_type(a)

Produces this crash:

» mypy ex.py --show-traceback
ex.py:8: error: INTERNAL ERROR -- Please try using mypy master on Github:
https://mypy.readthedocs.io/en/stable/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.902
Traceback (most recent call last):
  File "mypy/checker.py", line 401, in accept
  File "mypy/nodes.py", line 777, in accept
  File "mypy/checker.py", line 3586, in visit_decorator
  File "mypy/checkexpr.py", line 936, in check_call
  File "mypy/checkexpr.py", line 917, in check_call
  File "mypy/checkexpr.py", line 1029, in check_callable_call
  File "mypy/checkexpr.py", line 738, in apply_function_plugin
  File "/Users/sobolev/Documents/github/typeclasses/classes/contrib/mypy/features/typeclass.py", line 161, in __call__
    typeclass, fullname = self._load_typeclass(ctx.type.args[1], ctx)
  File "/Users/sobolev/Documents/github/typeclasses/classes/contrib/mypy/features/typeclass.py", line 207, in _load_typeclass
    typeclass = type_loader.load_typeclass(
  File "/Users/sobolev/Documents/github/typeclasses/classes/contrib/mypy/typeops/type_loader.py", line 35, in load_typeclass
    typeclass_info = ctx.api.lookup_qualified(fullname)  # type: ignore
  File "mypy/checker.py", line 4747, in lookup_qualified
  File "mypy/checker.py", line 4743, in lookup
KeyError: 'Failed lookup: a'
ex.py:8: : note: use --pdb to drop into pdb

We need to turn this into a typing error.

Improve generics support

There are some cases right now, when Generic typeclasses do not work as they should, for example:

from classes import typeclass
from typing import TypeVar

X = TypeVar('X')

@typeclass
def copy(instance: X) -> X:
    ...

@copy.instance(int)
def _copy_int(instance: int) -> int:
    ...

reveal_type(copy(1))

Right now it reports that signature is not correct, but it is.

ex.py:10: error: Instance callback is incompatible "def (instance: builtins.int) -> builtins.int"; expected "def [X] (instance: builtins.int) -> X`-1"
ex.py:10: error: Instance "builtins.int" does not match original type "X`-1"
ex.py:14: error: Argument 1 to "copy" has incompatible type "int"; expected <nothing>

Make sure the generic definition of AssociatedType matches a typeclass instance

Let's say you have this code:

from typing import TypeVar

from classes import typeclass, AssociatedType

X = TypeVar('X')

class Compare(AssociatedType):
    ...

@typeclass(Compare)
def compare(instance: X, other: X) -> X:
    ...

As you can see, your typeclass is generic. But, associated type is not.
It might lead to invalid type inference in some cases:

from typing import TypeVar, Iterable, List

from classes import typeclass, AssociatedType

X = TypeVar('X')

class GetItem(AssociatedType):
    ...

@typeclass(GetItem)
def get_item(instance: Iterable[X], index: int) -> X:
    ...

@get_item.instance(list)
def _get_item_list(instance: List[X], index: int) -> X:
    ...

reveal_type(get_item([1, 2, 3], 1))
# Revealed type is "<nothing>"

It can be fixed by modifing GetItem type:

class GetItem(AssociatedType[X]):
    ...

reveal_type(get_item([1, 2, 3], 1))
# Revealed type is "builtins.int*"

We need to add an error message for this case.

Dependabot couldn't find a Pipfile for this project

Dependabot couldn't find a Pipfile for this project.

Dependabot requires a Pipfile to evaluate your project's current Python dependencies. It had expected to find one at the path: /docs/requirements.txt‎/Pipfile.

If this isn't a Python project, or if it is a library, you may wish to disable updates for it from within Dependabot.

View the update logs.

Type restrictions

For example, one may want to define a typeclass with type restriction, like this:

@typeclass
def map_(instance: Functor[T], function: Callable[[T], V] -> V:
    ...

So, when defining instances, we would have to watch for this contract.
This will typecheck:

@map_.instance(Maybe)
def map_(instance: Maybe[T], function: Callable[[T], V] -> V:
    return instance.map(function)

But, this will not typecheck:

@map_.instance(int)
def map_(instance: int, function: Callable[[int], V] -> V:
    return function(instance)

Check that `@typeclass` functions do not have body

When defining a typeclass, it is easy to forget that @typeclass definition must not have a body.

Probably, we can check it with mypy.

So, this must be an error:

@typeclass
def some(instance) -> None:
    x = 1

Getting type_argument from function annotation

Hey @sobolevn!

functools.singledispatch now supports using type annotations instead of passing an argument:

@singledispatch
def fun(arg, verbose=False):
    ...

@fun.register
def _(arg: int, verbose=False):
    ...

The old syntax still works, but in simple cases like here this helps reduce unnecessary repetition and, as a result, the code looks a bit cleaner.

Would you be interesting in accepting a PR implementing similar interface for _TypeClass.instance?

Support types with custom `__instancecheck__` method

This code does not work:

from classes import typeclass

class Meta(type):
    def __instancecheck__(self, other) -> bool:
        return other == 1

class Some(object, metaclass=Meta):
    ...

@typeclass
def some(instance) -> bool:
    ...


@some.instance(Some)
def _some_some(instance: Some) -> bool:
    return True


print(some(1))
# NotImplementedError: Missing matched typeclass instance for type: int

But, it should! For example, phantom-types use this method: https://github.com/antonagestam/phantom-types/blob/main/phantom/base.py#L28-L43

If we want to support them - we would need this feature.

Solution

I suggest that we need to add types with __instancecheck__ defined to both ._instances and ._protocols.
This way both of these cases would work:

print(some(Some()))
print(some(1))

Special case for `tuple`

When working with tuple, we need to be sure that it has the porper size. For example:

from classes import typeclass
from typing import Tuple, TypeVar

X = TypeVar('X')

@typeclass
def some(instance) -> str:
    ...

@some.instance(tuple)
def _some_tuple(instance: Tuple[X, X]) -> X:
    ...

This code should not be allowed, because Tuple[X, X] says that the size of instance will be 2. But, we cannot guarantee it.

Instead, this should be used:

from classes import typeclass
from typing import Tuple, TypeVar

X = TypeVar('X')

@typeclass
def some(instance) -> str:
    ...

@some.instance(tuple)
def _some_tuple(instance: Tuple[X, ...]) -> X:
    ...

Dependabot couldn't find a Pipfile for this project

Dependabot couldn't find a Pipfile for this project.

Dependabot requires a Pipfile to evaluate your project's current Python dependencies. It had expected to find one at the path: /docs/requirements.txt‎/Pipfile.

If this isn't a Python project, or if it is a library, you may wish to disable updates for it from within Dependabot.

View the update logs.

Dependabot can't resolve your Python dependency files

Dependabot can't resolve your Python dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Creating virtualenv classes-KwUktzQH-py3.9 in /home/dependabot/.cache/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies...

  PackageNotFound

  Package sphinxcontrib-mermaid (0.4.0) not found.

  at /usr/local/.pyenv/versions/3.9.0/lib/python3.9/site-packages/poetry/repositories/pool.py:144 in package
      140│                     self._packages.append(package)
      141│ 
      142│                     return package
      143│ 
    → 144│         raise PackageNotFound("Package {} ({}) not found.".format(name, version))
      145│ 
      146│     def find_packages(
      147│         self, dependency,
      148│     ):

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

View the update logs.

Make generic `Supports[Some[X]]` work

Right now, this does not work:

- case: typeclass_generic_definition_associated_type
  disable_cache: false
  skip: true
  main: |
    from typing import List, TypeVar
    from classes import typeclass, AssociatedType

    X = TypeVar('X')

    class Some(AssociatedType):
        ...

    @typeclass(Some)
    def some(instance, b: int) -> X:
        ...

    @some.instance(list)
    def _some_ex(instance: List[X], b: int) -> X:
        return instance[b]  # We need this line to test inner inference

    reveal_type(some(['a', 'b'], 0))  # N: Revealed type is 'builtins.str*'
    reveal_type(some([1, 2, 3], 0))  # N: Revealed type is 'builtins.int*'

Why?

Because the revealed signature of call is (instance: Union[List[X], Supports[Some]], b: int) -> X and generic inference breaks for some reason. It always returns <nothing>

We also need something like this to work:

x: Supports[Some[int]]

reveal_type(some(x))  # Revealed type: int

Probably we can hack how Supports is inserted into mro by adding type args.

mypy report error on simple usage

from typing import Iterable
from classes import typeclass
from dataclasses import dataclass

@typeclass
def greet(instance) -> str:
    ...

@greet.instance(Iterable, is_protocol=True)
def _greet_str(instance: Iterable) -> str:
    return "Iterable"


@dataclass
class Point(object):
    x:int
    y:int

@greet.instance(Point)
def _greet_point(instance)-> str:
    return f"Point({instance.x}-{instance.y})"

point: Point = Point(1,2)
print(greet(point))
print(greet([1, 2, 3]))

image

image

mypy.ini

[mypy]
ignore_missing_imports = True
allow_redefinition = false
check_untyped_defs = true
ignore_errors = false
implicit_reexport = false
local_partial_types = true
strict_optional = true
strict_equality = true
no_implicit_optional = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unreachable = true
warn_no_return = true


[mypy]
plugins =
  classes.contrib.mypy.classes_plugin

Update docs

I don't like current docs structure.

I need to rewrite it based on different topics:

  • Concept
  • Basics (including protocols)
  • Generics
  • Associated types and Supports

Support variadic generics for `AssociatedType`

The thing about AssociatedType is that its subclasses can be used with any amount of type arguments.
Examples:

      >>> from typing import TypeVar

      >>> A = TypeVar('A')
      >>> B = TypeVar('B')
      >>> C = TypeVar('C')

      >>> class WithOne(AssociatedType[A]):
      ...    ...

      >>> class WithTwo(AssociatedType[A, B]):
      ...    ...

      >>> class WithThree(AssociatedType[A, B, C]):
      ...    ...

To achieve this I need:

  1. Runtime support, regular Generic does not allow it
  2. mypy support, probably via get_type_analyze_hook

False no-untyped-def

Issue

When disallow_untyped_defs is enabled for Mypy the initial @typeclass function will trigger a no-untyped-def error:

from classes import typeclass


@typeclass
def meow(instance) -> None:
    raise NotImplementedError(instance)


@meow.instance(str)
def _meow_str(instance: str) -> None:
    print(f"meow {instance}")
$ mypy test.py 
test.py: note: In function "meow":
test.py:5:1: error: Function is missing a type annotation for one or more arguments  [no-untyped-def]
Found 1 error in 1 file (checked 1 source file)

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.