Giter Club home page Giter Club logo

extype's Introduction

ExType

Extype stands for extensible types, a Python package that enables extending types.

PyPI version

Windows MacOS Linux

Installation

pip install extype

Alternatively, you can install through git (make sure to have pip 22.0 or higher):

python -m pip install --upgrade pip
pip install git+https://github.com/xpodev/extype/

Usage

First, in your Python code, import the package:

import extype

Then, you can use the built-in extensions for the builtin types. In order to apply these extensions, import the extend_all module from the extype.builtin_extensions package:

from extype.builtin_extensions import extend_all

This will apply the extensions to all builtins we support, as a side-effect of the import (to extend only some builtins, you can use the dedicated modules, see below).

Sometimes you don't want to apply all the extensions that we provide, but only for some specific type(s).

Say, for example, we only want to apply the provided extensions for list. We'll need to manually apply them like so:

from extype.builtin_extensions import list_ext

list_ext.extend()

Note: All built-in extension modules have an extend function which will apply the extensions in the module to the relevant type.

Currently, we provide the following extensions:

file extended types
dict_ext.py dict_keys, dict_values, dict_items
float_ext.py float
function_ext.py FunctionType, LambdaType
int_ext.py int
list_ext.py list
seq_ext.py map, filter, range, zip
str_ext.py str

Then you can use these extensions. Here's an example of using the list.map extension:

print([1, 2, 3].map(lambda x: x + 1))  # [2, 3, 4]

There's a list of all the built-in extensions here

Creating your own extensions

You can create your own extension methods, with which you can extend any type you want! (not only builtins) For example, let's make our own tofloat function in the int type. What we want to have at the end is:

x = 10
print(isinstance(x.tofloat(), float))  # True

First, we'll need some tools:

from extype import extension, extend_type_with

Next, we'll define our class which will hold the extension method. Note that this class will not get instantiated. It is also recommended to make this class inherit the type you want to extend, so you get better typing support.

class IntExtension(int):  # inheriting `int` for typing
  @extension  # marks this method to be added as an extension
  def tofloat(self):  # self will be of the same type we extend, which, in this case, is `int`
    return float(self)  # convert the int to float and return the result

After we create the class which will contain the extension methods, we need to apply them to the types we want to extend:

extend_type_with(int, IntExtension)

Now, we can run the code from above:

x = 10
print(isinstance(x.tofloat(), float))  # True

We can also apply multiple extensions to the same type or even the same extension to multiple types.

Only methods marked with @extension will be added as extension methods.

Note: Extending a type will extend it in all modules, not just the one that called the extend_type_with, so make sure you don't override an existing function, unless, of course, it is what you want.

Features

  • Exteranlly extend type via another type
  • Basic support for magic method extensions
    • Number protocol
    • Mapping protocol
    • Sequence protocol
  • Add support for reverse methods (e.g. __radd__)
  • Make this features/todo list look nicer
  • Add support for the rich comparison function

Maintainers

Build & Installation

We use Hatch to manage the build environment, and mesonpy to build the package.

Note: Currently, we use unreleased mesonpy features, so we install it from git.

First, install Hatch: https://hatch.pypa.io/latest/install/. We recommend using pipx.

After you've installed Hatch, you can build the package with the following command:

hatch run install_editable

With this, you can start using the package in your code. Spawn shell within the build environment:

hatch shell

It'll rebuild the package every time you import it, so you can test your changes. If you don't want to rebuild the package every time you import it, you can install it with:

hatch run install

But note that any changes you make won't be reflected in the installed package.

To build the wheel, you can use:

hatch run dist:build

This will build the wheel for all python versions, and put it in the dist folder.

Testing

To run tests for all python versions, run:

hatch run dist:test

To run tests for a specific python version, run:

hatch run +py=39 dist:test

Both commands will build, install the package into an isolated environment, and run the tests in it.

Built-in Extensions

Note: All of the following list extensions also exist on dict_keys, dict_values and dict_items.

list.all(self: List[T], fn: Callable[[T], bool] = bool) -> bool

Returns true if all elements, mapped through the given fn, are True.

list.any(self: List[T], fn: Callable[[T], bool] = bool) -> bool

Returns true if any of the elements, mapped through the given fn, is True.

list.map(self: List[T], fn: Callable[[T], U]) -> List[U]

Returns a new list whose elements are the result of applying the given function on each element in the original list.

list.reduce(self: List[T], fn: Callable[[T, T], T]) -> T

Reduces the list to a single value, using the given function as the reduction (combination) function.

Raises TypeError if the list is empty.

list.reduce(self: List[T], fn: Callable[[U, T], U], initial_value: U) -> U

Reduces the list to a single value, using the given function as the reduction (combination) function and the initial value.

list.filter(self: List[T], fn: Callable[[T], bool]) -> List[T]

Returns a new list containing all the elements that match the given predicate fn.

list.first(self: List[T]) -> T, raise IndexError

Returns the first element in the list, or raises an IndexError if the list is empty.

list.last(self: List[T]) -> T, raise IndexError

Returns the last element in the list, or raises IndexError if the list is empty.

float.round(self: float) -> int

Rounds the floating point number to the nearest integer.

float.round(self: float, ndigits: int) -> int | float

Round the floating point number to the nearest float with ndigits fraction digits.

# function @ functioin
function.__matmul__(self: Callable[[T], U], other: Callable[..., T]) -> Callable[..., U]

Compose 2 functions such that doing (foo @ bar)(*args, **kwargs) will have the same result as calling foo(bar(*args, **kwargs)).

int.hex(self: int) -> str

Returns the hexadecimal representation of the integer.

int.oct(self: int) -> str

Returns the octal representation of the integer.

int.bin(self: int) -> str

Returns the binary representation of the integer.

str.to_int(self: str, base: int = 10, default: T = ...) -> int | T

Converts the given string to an int with the given base. If it can't be converted and default is given, it is returned. Otherwise, a ValueError is thrown.

str.to_float(self: str, default: T = ...) -> float | T

Converts the given string to a float. If it can't be converted and default is given, it is returned. Otherwise, a ValueError is thrown.

  • The following extensions are valid for map, filter, range and zip
.tolist(self: Iterable[T]) -> List[T]

Exhausts the iterble and creates a list from it.

.map(self: Iterable[T], fn: Callable[[T], U]) -> Iterable[U]

Maps the iterable with the given function to create a new iterable.

This does not iterates through the original iterable.

.filter(self: Iterable[T], fn: Callable[[T], bool]) -> Iterable[T]

Filters the iterable with the given function as the predicate function.

This does not iterates through the original iterable.

extype's People

Contributors

binyamin555 avatar neriyaco avatar elazarcoh avatar

Stargazers

Thane Gill avatar  avatar Christian Kagerer avatar  avatar Daniel Mar avatar liangchen avatar  avatar

Watchers

 avatar  avatar

Forkers

binyamin555 dkmar

extype's Issues

Build & Publish actions

It would make our lives easier if we make it so whenever there's a merge to the main branch, it'll build the package and run the tests.

Also, if possible, whenever we create a release, also upload it to PyPI

Move to a modern build system

Maybe mesonpy? (it has editable build support in the version โ‰ฅ 0.13, which isn't released, need to pip install git+https)

Add built-in extensions

It may be thought of as part of #13 in the sense that each extension type should be applied separately.

So users can do:

from extype import extensions

extensions.apply()  # all
extensions.apply(list, int)  # only for int & list

I don't know if it's the best idea, so you should also suggest the way you think is best to do this.

Add async function extensions

We can (optionally) extend coroutine objects with the following extensions:

def then(Awaitable[T], Callable[[T], Awaitable[U] | U]) -> Awaitable[U]

def catch(Awaitable[T], Callable[[E], Awaitable[U] | U], *, exception: type[E] = Excpetion) -> Awaitable[T | U]

The then function basically maps the result of the first awaitable via an optionally async function. If the function is async, it is awaited in the context of the wrapped awaitable.

The catch function catches an exception of the given type and the passed function is called with the caught exception.

If no exception was raised inside the wrapped awaitable, the function will not be called.

The passed function can optionally return a value to be returned in case of an error.

The passed function can be either sync or async. If it's async, it is awaited in the context of the wrapped awaitable.

Implementation Suggestion

First, create 2 wrapping functions:

from inspect import iscoroutinefunction as is_async

async def then(awaitable, fn):
  result = fn(await awaitable)
  if is_async(fn):
    return await result
  return result

async def catch(awaitable, fn, *, exception=Exception):
  try:
    return await awaitable
  except exception as e:
    result = fn(e)
    if is_async(fn):
      return await result
    return result

Then, the extension methods will call these respectively, passing self as the awaitable and the other arguments as is.

A PR implementing this should also consider updating README.md to include the added built-in extension.

String extensions

Add a to_int method with the following signature.

def to_int(self: str, base: int = 10, __fallback: _T = _SENTINEL) -> int | _T

The method returns the integer represented by the string according to the given base parameter.

If the string does not represent a valid integer in the given base and the __fallback parameter is _SENTINEL, this method throws a ValueError exception. Otherwise, the given __fallback value is returned.

Same goes for float (i.e. to_float) except it doesn't have a base parameter.

def to_float(self: str, __fallback: _T = _SENTINEL) -> float | _T

Installation does not work

When installed via pip on Python 3.9 and 3.11 on Windows trying to call from extype import extension, extend_type_with I get this message: ImportError: DLL load failed while importing core: The module was not found. I tried 2 reinstalls on each version with no change.

While trying to install from git with pip install git+https://github.com/xpodev/extype I get this error

The Meson build system
      [...]
      Build type: native build
      Project name: extype
      Project version: 1.0.0
      C++ compiler for the host machine: c++ (gcc 6.3.0 "c++ (MinGW.org GCC-6.3.0-1) 6.3.0")
      C++ linker for the host machine: c++ ld.bfd 2.28
      Host machine cpu family: x86
      Host machine cpu: x86
      Program python found: YES
      Need python for x86, but found x86_64
      Run-time dependency python found: NO (tried sysconfig)

Auto-extend builtins

We should consider what would be the best user experience for auto-extending builtins.
Let's add options:

  1. Have a module that upon import would extend all builtins we have. Must give the module a name that would imply this.
  2. For each type, have a module that does auto-extending. (I don't think it's really needed)
  3. Don't have anything that auto-extend on import. Require to do it explicitly by calling a function.

Rename package name

I think we should have a consistent name for this package.
In PyPi it's extype but the usage is extypes.
Because extypes is already registered on PyPi, I suggest we rename this repo and the package name to extype.

Organize code base

In the current project layout, all functionalities written in Python are placed in the package's __init__.py file.
This makes things compact on the one hand, but a bit messy on the other hand.

We should probably split the functionalities, so each one has its own file.

Mainly:

  • Builtin-extensions
  • Extension tools (extension decorator and the extend_type_with function)
  • Core utilities (i.e. the functions which should not be used by users, such as get_builtin_type_dict, which, BTW, should be renamed)

Add more test cases

As of now, our test cases do not cover much of the features we have.
We should add more test cases, so that we know most (if not all) things work as expected, including:

  • Test for each magic method and its reverse method
  • Test for property and any descriptor in general
  • Test for the built-in extensions that we provide

Extend built in types properties

It would be very nice if we'll have an option to extend the properties of built in types as well.

Example:

class ListExtension(list):
    @extension
    @property
    def length(self):
        return len(self)


extend_type_with(list, ListExtension)

a = list(range(10))
print(a.length)

Currently the module raises a TypeError as the property is not callable

def extension(fn: _T) -> _T:
    if not callable(fn): # <-- Here
        raise TypeError
    fn._extension_method = True
    return fn

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.