diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | python-expression.spec | 1588 | ||||
-rw-r--r-- | sources | 1 |
3 files changed, 1590 insertions, 0 deletions
@@ -0,0 +1 @@ +/expression-4.2.4.tar.gz diff --git a/python-expression.spec b/python-expression.spec new file mode 100644 index 0000000..faea335 --- /dev/null +++ b/python-expression.spec @@ -0,0 +1,1588 @@ +%global _empty_manifest_terminate_build 0 +Name: python-expression +Version: 4.2.4 +Release: 1 +Summary: Practical functional programming for Python 3.9+ +License: MIT +URL: https://github.com/cognitedata/Expression +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/6d/65/c20876c9d96db9a7c52c72c2bd00137a7556600ceeccf3ddeaedb54249e9/expression-4.2.4.tar.gz +BuildArch: noarch + +Requires: python3-typing-extensions + +%description +# Expression + +[](https://pypi.python.org/pypi/Expression) + + +[](https://expression.readthedocs.io/en/latest/?badge=latest) +[](https://codecov.io/gh/cognitedata/expression) + +> Pragmatic functional programming + +Expression aims to be a solid, type-safe, pragmatic, and high performance +library for frictionless and practical functional programming in Python 3.9+. + +By pragmatic, we mean that the goal of the library is to use simple abstractions +to enable you to do practical and productive functional programming in Python +(instead of being a [Monad tutorial](https://github.com/dbrattli/OSlash)). + +Python is a multi-paradigm programming language that also supports functional +programming constructs such as functions, higher-order functions, lambdas, and +in many ways favors composition over inheritance. + +> Better Python with F# + +Expression tries to make a better Python by providing several functional +features inspired by [F#](https://fsharp.org). This serves several +purposes: + +- Enable functional programming in a Pythonic way, i.e., make sure we are not + over-abstracting things. Expression will not require purely functional + programming as would a language like Haskell. +- Everything you learn with Expression can also be used with F#. Learn F# by + starting in a programming language they already know. Perhaps get inspired to + also [try out F#](https://aka.ms/fsharphome) by itself. +- Make it easier for F# developers to use Python when needed, and re-use many + of the concepts and abstractions they already know and love. + +Expression will enable you to work with Python using many of the same +programming concepts and abstractions. This enables concepts such as [Railway +oriented programming](https://fsharpforfunandprofit.com/rop/) (ROP) for better +and predictable error handling. Pipelining for workflows, computational +expressions, etc. + +> _Expressions evaluate to a value. Statements do something._ + +F# is a functional programming language for .NET that is succinct (concise, +readable, and type-safe) and kind of +[Pythonic](https://docs.python.org/3/glossary.html). F# is in many ways very +similar to Python, but F# can also do a lot of things better than Python: + +- Strongly typed, if it compiles it usually works making refactoring much + safer. You can trust the type-system. With [mypy](http://mypy-lang.org/) or + [Pylance](https://github.com/microsoft/pylance-release) you often wonder who + is right and who is wrong. +- Type inference, the compiler deduces types during compilation +- Expression based language + +## Getting Started + +You can install the latest `expression` from PyPI by running `pip` (or +`pip3`). Note that `expression` only works for Python 3.9+. + +```console +> pip3 install expression +``` + +## Goals + +- Industrial strength library for functional programming in Python. +- The resulting code should look and feel like Python + ([PEP-8](https://www.python.org/dev/peps/pep-0008/)). We want to make a + better Python, not some obscure DSL or academic Monad tutorial. +- Provide pipelining and pipe friendly methods. Compose all the things! +- Dot-chaining on objects as an alternative syntax to pipes. +- Lower the cognitive load on the programmer by: + - Avoid currying, not supported in Python by default and not a well known + concept by Python programmers. + - Avoid operator (`|`, `>>`, etc) overloading, this usually confuses more + than it helps. + - Avoid recursion. Recursion is not normally used in Python and any use of it + should be hidden within the SDK. +- Provide [type-hints](https://docs.python.org/3/library/typing.html) for all + functions and methods. +- Support PEP 634 and structural pattern matching. +- Code must pass strict static type checking by + [pylance](https://devblogs.microsoft.com/python/announcing-pylance-fast-feature-rich-language-support-for-python-in-visual-studio-code/). + Pylance is awesome, use it! +- [Pydantic](https://pydantic-docs.helpmanual.io/) friendly data types. Use Expression + types as part of your Pydantic data model and (de)serialize to/from JSON. + +## Supported features + +Expression will never provide you with all the features of F# and .NET. We are +providing a few of the features we think are useful, and will add more +on-demand as we go along. + +- **Pipelining** - for creating workflows. +- **Composition** - for composing and creating new operators. +- **Fluent or Functional** syntax, i.e., dot chain or pipeline operators. +- **Pattern Matching** - an alternative flow control to `if-elif-else`. +- **Error Handling** - Several error handling types. + - **Option** - for optional stuff and better `None` handling. + - **Result** - for better error handling and enables railway-oriented + programming in Python. + - **Try** - a simpler result type that pins the error to an Exception. +- **Collections** - immutable collections. + - **TypedArray** - a generic array type that abstracts the details of + `bytearray`, `array.array` and `list` modules. + - **Sequence** - a better + [itertools](https://docs.python.org/3/library/itertools.html) and + fully compatible with Python iterables. + - **Block** - a frozen and immutable list type. + - **Map** - a frozen and immutable dictionary type. + - **AsyncSeq** - Asynchronous iterables. + - **AsyncObservable** - Asynchronous observables. Provided separately + by [aioreactive](https://github.com/dbrattli/aioreactive). +- **Data Modelling** - sum and product types + - **TaggedUnion** - A tagged (discriminated) union type. +- **Parser Combinators** - A recursive decent string parser combinator + library. +- **Effects**: - lightweight computational expressions for Python. This + is amazing stuff. + - **option** - an optional world for working with optional values. + - **result** - an error handling world for working with result values. +- **Mailbox Processor**: for lock free programming using the [Actor + model](https://en.wikipedia.org/wiki/Actor_model). +- **Cancellation Token**: for cancellation of asynchronous (and + synchronous) workflows. +- **Disposable**: For resource management. + +### Pipelining + +Expression provides a `pipe` function similar to `|>` in F#. We don't want to +overload any Python operators, e.g., `|` so `pipe` is a plain old function taking +N-arguments, and will let you pipe a value through any number of functions. + +```python +from expression import pipe + +v = 1 +fn = lambda x: x + 1 +gn = lambda x: x * 2 + +assert pipe(v, fn, gn) == gn(fn(v)) +``` + +Expression objects (e.g., `Some`, `Seq`, `Result`) also have a `pipe` method, so you can dot chain pipelines +directly on the object: + +```python +from expression import Some + +v = Some(1) +fn = lambda x: x.map(lambda y: y + 1) +gn = lambda x: x.map(lambda y: y * 2) + +assert v.pipe(fn, gn) == gn(fn(v)) +``` + +So for example with sequences you may create sequence transforming +pipelines: + +```python +from expression.collections import seq, Seq + +xs = Seq.of(9, 10, 11) +ys = xs.pipe( + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0) +) + +assert ys == 110 +``` + +### Composition + +Functions may even be composed directly into custom operators: + +```python +from expression import compose +from expression.collections import seq, Seq + +xs = Seq.of(9, 10, 11) +custom = compose( + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0) +) +ys = custom(xs) + +assert ys == 110 +``` + +### Fluent and Functional + +Expression can be used both with a fluent or functional syntax (or both.) + +#### Fluent syntax + +The fluent syntax uses methods and is very compact. But it might get you into +trouble for large pipelines since it's not a natural way of adding line breaks. + +```python +from expression.collections import Seq + +xs = Seq.of(1, 2, 3) +ys = xs.map(lambda x: x * 100).filter(lambda x: x > 100).fold(lambda s, x: s + x, 0) +``` + +Note that fluent syntax is probably the better choice if you use mypy +for type checking since mypy may have problems inferring types through +larger pipelines. + +#### Functional syntax + +The functional syntax is a bit more verbose but you can easily add new +operations on new lines. The functional syntax is great to use together +with pylance/pyright. + +```python +from expression import pipe +from expression.collections import seq, Seq + +xs = Seq.of(1, 2, 3) +ys = pipe(xs, + seq.map(lambda x: x * 100), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0), +) +``` + +Both fluent and functional syntax may be mixed and even pipe can be used +fluently. + +```python +from expression.collections import seq, Seq +xs = Seq.of(1, 2, 3).pipe(seq.map(...)) +``` + +### Option + +The `Option` type is used when a function or method cannot produce a meaningful +output for a given input. + +An option value may have a value of a given type, i.e., `Some(value)`, or it might +not have any meaningful value, i.e., `Nothing`. + +```python +from expression import Some, Nothing, Option + +def keep_positive(a: int) -> Option[int]: + if a > 0: + return Some(a) + + return Nothing +``` + +```python +from expression import Option, Ok +def exists(x : Option[int]) -> bool: + match x: + case Some(_): + return True + return False +``` + +### Option as an effect + +Effects in Expression is implemented as specially decorated coroutines +([enhanced generators](https://www.python.org/dev/peps/pep-0342/)) using +`yield`, `yield from` and `return` to consume or generate optional values: + +```python +from expression import effect, Some + +@effect.option[int]() +def fn(): + x = yield 42 + y = yield from Some(43) + + return x + y + +xs = fn() +``` + +This enables ["railway oriented +programming"](https://fsharpforfunandprofit.com/rop/), e.g., if one part of the +function yields from `Nothing` then the function is side-tracked +(short-circuit) and the following statements will never be executed. The end +result of the expression will be `Nothing`. Thus results from such an option +decorated function can either be `Ok(value)` or `Error(error_value)`. + +```python +from expression import effect, Some, Nothing + +@effect.option[int]() +def fn(): + x = yield from Nothing # or a function returning Nothing + + # -- The rest of the function will never be executed -- + y = yield from Some(43) + + return x + y + +xs = fn() +assert xs is Nothing +``` + +For more information about options: + +- [Tutorial](https://expression.readthedocs.io/en/latest/tutorial/optional_values.html) +- [API reference](https://expression.readthedocs.io/en/latest/reference/option.html) + +### Result + +The `Result[T, TError]` type lets you write error-tolerant code that can be +composed. A Result works similar to `Option`, but lets you define the value used +for errors, e.g., an exception type or similar. This is great when you want to +know why some operation failed (not just `Nothing`). This type serves the same +purpose of an `Either` type where `Left` is used for the error condition and `Right` +for a success value. + +```python +from expression import effect, Ok, Result + +@effect.result[int, Exception]() +def fn(): + x = yield from Ok(42) + y = yield from Ok(10) + return x + y + +xs = fn() +assert isinstance(xs, Result) +``` + +A simplified type called `Try` is also available. It's a result type that is +pinned to `Exception` i.e., `Result[TSource, Exception]`. + +### Sequence + +Sequences is a thin wrapper on top of iterables and contains operations for working with +Python iterables. Iterables are immutable by design, and perfectly suited for functional +programming. + +```python +import functools +from expression import pipe +from expression.collections import seq + +# Normal python way. Nested functions are hard to read since you need to +# start reading from the end of the expression. +xs = range(100) +ys = functools.reduce(lambda s, x: s + x, filter(lambda x: x > 100, map(lambda x: x * 10, xs)), 0) + +# With Expression, you pipe the result, so it flows from one operator to the next: +zs = pipe( + xs, + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0), +) +assert ys == zs +``` + +## Tagged Unions + +Tagged Unions (aka discriminated unions) may look similar to normal Python Unions. But +they are [different](https://stackoverflow.com/a/61646841) in that the operands in a +type union `(A | B)` are both types, while the cases in a tagged union type `U = A | B` +are both constructors for the type U and are not types themselves. One consequence is +that tagged unions can be nested in a way union types might not. + +In Expression you make a tagged union by defining your type as a sub-class of +`TaggedUnion` with the appropriate generic types that this union represent for +each case. Then you define static or class-method constructors for creating each of the +tagged union cases. + +```python +from dataclasses import dataclass +from expression import TaggedUnion, tag + +@dataclass +class Rectangle: + width: float + length: float + +@dataclass +class Circle: + radius: float + +class Shape(TaggedUnion): + RECTANGLE = tag(Rectangle) + CIRCLE = tag(Circle) + + @staticmethod + def rectangle(width: float, length: float) -> Shape: + return Shape(Shape.RECTANGLE, Rectangle(width, length)) + + @staticmethod + def circle(radius: float) -> Shape: + return Shape(Shape.CIRCLE, Circle(radius)) +``` + +Now you may pattern match the shape to get back the actual value: + +```python + from expression import match + + shape = Shape.Rectangle(2.3, 3.3) + + match shape: + case Shape(value=Rectangle(width=2.3)): + assert shape.value.width == 2.3 + case _: + assert False +``` + +## Notable differences between Expression and F# + +In F# modules are capitalized, in Python they are lowercase +([PEP-8](https://www.python.org/dev/peps/pep-0008/#package-and-module-names)). +E.g in F# `Option` is both a module (`OptionModule` internally) and a +type. In Python the module is `option` and the type is capitalized i.e +`Option`. + +Thus in Expression you use `option` as the module to access module functions +such as `option.map` and the name `Option` for the type itself. + +```pycon +>>> from expression import Option, option +>>> Option +<class 'expression.core.option.Option'> +>>> option +<module 'expression.core.option' from '/Users/dbrattli/Developer/Github/Expression/expression/core/option.py'> +``` + +## Common Gotchas and Pitfalls + +A list of common problems and how you may solve it: + +### Expression is missing the function/operator I need + +Remember that everything is just a function, so you can easily implement +a custom function yourself and use it with Expression. If you think the +function is also usable for others, then please open a PR to include it +with Expression. + +## Resources and References + +A collection of resources that were used as reference and inspiration +for creating this library. + +- F# (http://fsharp.org) +- Get Started with F# (https://aka.ms/fsharphome) +- F# as a Better Python - Phillip Carter - NDC Oslo 2020 + (https://www.youtube.com/watch?v=_QnbV6CAWXc) +- OSlash (https://github.com/dbrattli/OSlash) +- RxPY (https://github.com/ReactiveX/RxPY) +- PEP 8 -- Style Guide for Python Code (https://www.python.org/dev/peps/pep-0008/) +- PEP 342 -- Coroutines via Enhanced Generators + (https://www.python.org/dev/peps/pep-0342/) +- PEP 380 -- Syntax for Delegating to a Subgenerator + (https://www.python.org/dev/peps/pep-0380) +- PEP 479 -- Change StopIteration handling inside generators (https://www.python.org/dev/peps/pep-0479/) +- PEP 634 -- Structural Pattern Matching (https://www.python.org/dev/peps/pep-0634/) +- Thunks, Trampolines and Continuation Passing + (https://jtauber.com/blog/2008/03/30/thunks,_trampolines_and_continuation_passing/) +- Tail Recursion Elimination + (http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html) +- Final Words on Tail Calls + (http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html) +- Python is the Haskell You Never Knew You Had: Tail Call Optimization + (https://sagnibak.github.io/blog/python-is-haskell-tail-recursion/) + +## How-to Contribute + +You are very welcome to contribute with suggestions or PRs :heart_eyes: It is +nice if you can try to align the code and naming with F# modules, functions, +and documentation if possible. But submit a PR even if you should feel unsure. + +Code, doc-strings, and comments should also follow the [Google Python Style +Guide](https://google.github.io/styleguide/pyguide.html). + +Code checks are done using + +- [Black](https://github.com/psf/black) +- [flake8](https://github.com/PyCQA/flake8) +- [isort](https://github.com/PyCQA/isort) + +To run code checks on changed files every time you commit, install the pre-commit hooks +by running: + +```console +> pre-commit install +``` + +## Code of Conduct + +This project follows https://www.contributor-covenant.org, see our [Code +of +Conduct](https://github.com/cognitedata/Expression/blob/main/CODE_OF_CONDUCT.md). + +## License + +MIT, see [LICENSE](https://github.com/cognitedata/Expression/blob/main/LICENSE). + + +%package -n python3-expression +Summary: Practical functional programming for Python 3.9+ +Provides: python-expression +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-expression +# Expression + +[](https://pypi.python.org/pypi/Expression) + + +[](https://expression.readthedocs.io/en/latest/?badge=latest) +[](https://codecov.io/gh/cognitedata/expression) + +> Pragmatic functional programming + +Expression aims to be a solid, type-safe, pragmatic, and high performance +library for frictionless and practical functional programming in Python 3.9+. + +By pragmatic, we mean that the goal of the library is to use simple abstractions +to enable you to do practical and productive functional programming in Python +(instead of being a [Monad tutorial](https://github.com/dbrattli/OSlash)). + +Python is a multi-paradigm programming language that also supports functional +programming constructs such as functions, higher-order functions, lambdas, and +in many ways favors composition over inheritance. + +> Better Python with F# + +Expression tries to make a better Python by providing several functional +features inspired by [F#](https://fsharp.org). This serves several +purposes: + +- Enable functional programming in a Pythonic way, i.e., make sure we are not + over-abstracting things. Expression will not require purely functional + programming as would a language like Haskell. +- Everything you learn with Expression can also be used with F#. Learn F# by + starting in a programming language they already know. Perhaps get inspired to + also [try out F#](https://aka.ms/fsharphome) by itself. +- Make it easier for F# developers to use Python when needed, and re-use many + of the concepts and abstractions they already know and love. + +Expression will enable you to work with Python using many of the same +programming concepts and abstractions. This enables concepts such as [Railway +oriented programming](https://fsharpforfunandprofit.com/rop/) (ROP) for better +and predictable error handling. Pipelining for workflows, computational +expressions, etc. + +> _Expressions evaluate to a value. Statements do something._ + +F# is a functional programming language for .NET that is succinct (concise, +readable, and type-safe) and kind of +[Pythonic](https://docs.python.org/3/glossary.html). F# is in many ways very +similar to Python, but F# can also do a lot of things better than Python: + +- Strongly typed, if it compiles it usually works making refactoring much + safer. You can trust the type-system. With [mypy](http://mypy-lang.org/) or + [Pylance](https://github.com/microsoft/pylance-release) you often wonder who + is right and who is wrong. +- Type inference, the compiler deduces types during compilation +- Expression based language + +## Getting Started + +You can install the latest `expression` from PyPI by running `pip` (or +`pip3`). Note that `expression` only works for Python 3.9+. + +```console +> pip3 install expression +``` + +## Goals + +- Industrial strength library for functional programming in Python. +- The resulting code should look and feel like Python + ([PEP-8](https://www.python.org/dev/peps/pep-0008/)). We want to make a + better Python, not some obscure DSL or academic Monad tutorial. +- Provide pipelining and pipe friendly methods. Compose all the things! +- Dot-chaining on objects as an alternative syntax to pipes. +- Lower the cognitive load on the programmer by: + - Avoid currying, not supported in Python by default and not a well known + concept by Python programmers. + - Avoid operator (`|`, `>>`, etc) overloading, this usually confuses more + than it helps. + - Avoid recursion. Recursion is not normally used in Python and any use of it + should be hidden within the SDK. +- Provide [type-hints](https://docs.python.org/3/library/typing.html) for all + functions and methods. +- Support PEP 634 and structural pattern matching. +- Code must pass strict static type checking by + [pylance](https://devblogs.microsoft.com/python/announcing-pylance-fast-feature-rich-language-support-for-python-in-visual-studio-code/). + Pylance is awesome, use it! +- [Pydantic](https://pydantic-docs.helpmanual.io/) friendly data types. Use Expression + types as part of your Pydantic data model and (de)serialize to/from JSON. + +## Supported features + +Expression will never provide you with all the features of F# and .NET. We are +providing a few of the features we think are useful, and will add more +on-demand as we go along. + +- **Pipelining** - for creating workflows. +- **Composition** - for composing and creating new operators. +- **Fluent or Functional** syntax, i.e., dot chain or pipeline operators. +- **Pattern Matching** - an alternative flow control to `if-elif-else`. +- **Error Handling** - Several error handling types. + - **Option** - for optional stuff and better `None` handling. + - **Result** - for better error handling and enables railway-oriented + programming in Python. + - **Try** - a simpler result type that pins the error to an Exception. +- **Collections** - immutable collections. + - **TypedArray** - a generic array type that abstracts the details of + `bytearray`, `array.array` and `list` modules. + - **Sequence** - a better + [itertools](https://docs.python.org/3/library/itertools.html) and + fully compatible with Python iterables. + - **Block** - a frozen and immutable list type. + - **Map** - a frozen and immutable dictionary type. + - **AsyncSeq** - Asynchronous iterables. + - **AsyncObservable** - Asynchronous observables. Provided separately + by [aioreactive](https://github.com/dbrattli/aioreactive). +- **Data Modelling** - sum and product types + - **TaggedUnion** - A tagged (discriminated) union type. +- **Parser Combinators** - A recursive decent string parser combinator + library. +- **Effects**: - lightweight computational expressions for Python. This + is amazing stuff. + - **option** - an optional world for working with optional values. + - **result** - an error handling world for working with result values. +- **Mailbox Processor**: for lock free programming using the [Actor + model](https://en.wikipedia.org/wiki/Actor_model). +- **Cancellation Token**: for cancellation of asynchronous (and + synchronous) workflows. +- **Disposable**: For resource management. + +### Pipelining + +Expression provides a `pipe` function similar to `|>` in F#. We don't want to +overload any Python operators, e.g., `|` so `pipe` is a plain old function taking +N-arguments, and will let you pipe a value through any number of functions. + +```python +from expression import pipe + +v = 1 +fn = lambda x: x + 1 +gn = lambda x: x * 2 + +assert pipe(v, fn, gn) == gn(fn(v)) +``` + +Expression objects (e.g., `Some`, `Seq`, `Result`) also have a `pipe` method, so you can dot chain pipelines +directly on the object: + +```python +from expression import Some + +v = Some(1) +fn = lambda x: x.map(lambda y: y + 1) +gn = lambda x: x.map(lambda y: y * 2) + +assert v.pipe(fn, gn) == gn(fn(v)) +``` + +So for example with sequences you may create sequence transforming +pipelines: + +```python +from expression.collections import seq, Seq + +xs = Seq.of(9, 10, 11) +ys = xs.pipe( + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0) +) + +assert ys == 110 +``` + +### Composition + +Functions may even be composed directly into custom operators: + +```python +from expression import compose +from expression.collections import seq, Seq + +xs = Seq.of(9, 10, 11) +custom = compose( + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0) +) +ys = custom(xs) + +assert ys == 110 +``` + +### Fluent and Functional + +Expression can be used both with a fluent or functional syntax (or both.) + +#### Fluent syntax + +The fluent syntax uses methods and is very compact. But it might get you into +trouble for large pipelines since it's not a natural way of adding line breaks. + +```python +from expression.collections import Seq + +xs = Seq.of(1, 2, 3) +ys = xs.map(lambda x: x * 100).filter(lambda x: x > 100).fold(lambda s, x: s + x, 0) +``` + +Note that fluent syntax is probably the better choice if you use mypy +for type checking since mypy may have problems inferring types through +larger pipelines. + +#### Functional syntax + +The functional syntax is a bit more verbose but you can easily add new +operations on new lines. The functional syntax is great to use together +with pylance/pyright. + +```python +from expression import pipe +from expression.collections import seq, Seq + +xs = Seq.of(1, 2, 3) +ys = pipe(xs, + seq.map(lambda x: x * 100), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0), +) +``` + +Both fluent and functional syntax may be mixed and even pipe can be used +fluently. + +```python +from expression.collections import seq, Seq +xs = Seq.of(1, 2, 3).pipe(seq.map(...)) +``` + +### Option + +The `Option` type is used when a function or method cannot produce a meaningful +output for a given input. + +An option value may have a value of a given type, i.e., `Some(value)`, or it might +not have any meaningful value, i.e., `Nothing`. + +```python +from expression import Some, Nothing, Option + +def keep_positive(a: int) -> Option[int]: + if a > 0: + return Some(a) + + return Nothing +``` + +```python +from expression import Option, Ok +def exists(x : Option[int]) -> bool: + match x: + case Some(_): + return True + return False +``` + +### Option as an effect + +Effects in Expression is implemented as specially decorated coroutines +([enhanced generators](https://www.python.org/dev/peps/pep-0342/)) using +`yield`, `yield from` and `return` to consume or generate optional values: + +```python +from expression import effect, Some + +@effect.option[int]() +def fn(): + x = yield 42 + y = yield from Some(43) + + return x + y + +xs = fn() +``` + +This enables ["railway oriented +programming"](https://fsharpforfunandprofit.com/rop/), e.g., if one part of the +function yields from `Nothing` then the function is side-tracked +(short-circuit) and the following statements will never be executed. The end +result of the expression will be `Nothing`. Thus results from such an option +decorated function can either be `Ok(value)` or `Error(error_value)`. + +```python +from expression import effect, Some, Nothing + +@effect.option[int]() +def fn(): + x = yield from Nothing # or a function returning Nothing + + # -- The rest of the function will never be executed -- + y = yield from Some(43) + + return x + y + +xs = fn() +assert xs is Nothing +``` + +For more information about options: + +- [Tutorial](https://expression.readthedocs.io/en/latest/tutorial/optional_values.html) +- [API reference](https://expression.readthedocs.io/en/latest/reference/option.html) + +### Result + +The `Result[T, TError]` type lets you write error-tolerant code that can be +composed. A Result works similar to `Option`, but lets you define the value used +for errors, e.g., an exception type or similar. This is great when you want to +know why some operation failed (not just `Nothing`). This type serves the same +purpose of an `Either` type where `Left` is used for the error condition and `Right` +for a success value. + +```python +from expression import effect, Ok, Result + +@effect.result[int, Exception]() +def fn(): + x = yield from Ok(42) + y = yield from Ok(10) + return x + y + +xs = fn() +assert isinstance(xs, Result) +``` + +A simplified type called `Try` is also available. It's a result type that is +pinned to `Exception` i.e., `Result[TSource, Exception]`. + +### Sequence + +Sequences is a thin wrapper on top of iterables and contains operations for working with +Python iterables. Iterables are immutable by design, and perfectly suited for functional +programming. + +```python +import functools +from expression import pipe +from expression.collections import seq + +# Normal python way. Nested functions are hard to read since you need to +# start reading from the end of the expression. +xs = range(100) +ys = functools.reduce(lambda s, x: s + x, filter(lambda x: x > 100, map(lambda x: x * 10, xs)), 0) + +# With Expression, you pipe the result, so it flows from one operator to the next: +zs = pipe( + xs, + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0), +) +assert ys == zs +``` + +## Tagged Unions + +Tagged Unions (aka discriminated unions) may look similar to normal Python Unions. But +they are [different](https://stackoverflow.com/a/61646841) in that the operands in a +type union `(A | B)` are both types, while the cases in a tagged union type `U = A | B` +are both constructors for the type U and are not types themselves. One consequence is +that tagged unions can be nested in a way union types might not. + +In Expression you make a tagged union by defining your type as a sub-class of +`TaggedUnion` with the appropriate generic types that this union represent for +each case. Then you define static or class-method constructors for creating each of the +tagged union cases. + +```python +from dataclasses import dataclass +from expression import TaggedUnion, tag + +@dataclass +class Rectangle: + width: float + length: float + +@dataclass +class Circle: + radius: float + +class Shape(TaggedUnion): + RECTANGLE = tag(Rectangle) + CIRCLE = tag(Circle) + + @staticmethod + def rectangle(width: float, length: float) -> Shape: + return Shape(Shape.RECTANGLE, Rectangle(width, length)) + + @staticmethod + def circle(radius: float) -> Shape: + return Shape(Shape.CIRCLE, Circle(radius)) +``` + +Now you may pattern match the shape to get back the actual value: + +```python + from expression import match + + shape = Shape.Rectangle(2.3, 3.3) + + match shape: + case Shape(value=Rectangle(width=2.3)): + assert shape.value.width == 2.3 + case _: + assert False +``` + +## Notable differences between Expression and F# + +In F# modules are capitalized, in Python they are lowercase +([PEP-8](https://www.python.org/dev/peps/pep-0008/#package-and-module-names)). +E.g in F# `Option` is both a module (`OptionModule` internally) and a +type. In Python the module is `option` and the type is capitalized i.e +`Option`. + +Thus in Expression you use `option` as the module to access module functions +such as `option.map` and the name `Option` for the type itself. + +```pycon +>>> from expression import Option, option +>>> Option +<class 'expression.core.option.Option'> +>>> option +<module 'expression.core.option' from '/Users/dbrattli/Developer/Github/Expression/expression/core/option.py'> +``` + +## Common Gotchas and Pitfalls + +A list of common problems and how you may solve it: + +### Expression is missing the function/operator I need + +Remember that everything is just a function, so you can easily implement +a custom function yourself and use it with Expression. If you think the +function is also usable for others, then please open a PR to include it +with Expression. + +## Resources and References + +A collection of resources that were used as reference and inspiration +for creating this library. + +- F# (http://fsharp.org) +- Get Started with F# (https://aka.ms/fsharphome) +- F# as a Better Python - Phillip Carter - NDC Oslo 2020 + (https://www.youtube.com/watch?v=_QnbV6CAWXc) +- OSlash (https://github.com/dbrattli/OSlash) +- RxPY (https://github.com/ReactiveX/RxPY) +- PEP 8 -- Style Guide for Python Code (https://www.python.org/dev/peps/pep-0008/) +- PEP 342 -- Coroutines via Enhanced Generators + (https://www.python.org/dev/peps/pep-0342/) +- PEP 380 -- Syntax for Delegating to a Subgenerator + (https://www.python.org/dev/peps/pep-0380) +- PEP 479 -- Change StopIteration handling inside generators (https://www.python.org/dev/peps/pep-0479/) +- PEP 634 -- Structural Pattern Matching (https://www.python.org/dev/peps/pep-0634/) +- Thunks, Trampolines and Continuation Passing + (https://jtauber.com/blog/2008/03/30/thunks,_trampolines_and_continuation_passing/) +- Tail Recursion Elimination + (http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html) +- Final Words on Tail Calls + (http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html) +- Python is the Haskell You Never Knew You Had: Tail Call Optimization + (https://sagnibak.github.io/blog/python-is-haskell-tail-recursion/) + +## How-to Contribute + +You are very welcome to contribute with suggestions or PRs :heart_eyes: It is +nice if you can try to align the code and naming with F# modules, functions, +and documentation if possible. But submit a PR even if you should feel unsure. + +Code, doc-strings, and comments should also follow the [Google Python Style +Guide](https://google.github.io/styleguide/pyguide.html). + +Code checks are done using + +- [Black](https://github.com/psf/black) +- [flake8](https://github.com/PyCQA/flake8) +- [isort](https://github.com/PyCQA/isort) + +To run code checks on changed files every time you commit, install the pre-commit hooks +by running: + +```console +> pre-commit install +``` + +## Code of Conduct + +This project follows https://www.contributor-covenant.org, see our [Code +of +Conduct](https://github.com/cognitedata/Expression/blob/main/CODE_OF_CONDUCT.md). + +## License + +MIT, see [LICENSE](https://github.com/cognitedata/Expression/blob/main/LICENSE). + + +%package help +Summary: Development documents and examples for expression +Provides: python3-expression-doc +%description help +# Expression + +[](https://pypi.python.org/pypi/Expression) + + +[](https://expression.readthedocs.io/en/latest/?badge=latest) +[](https://codecov.io/gh/cognitedata/expression) + +> Pragmatic functional programming + +Expression aims to be a solid, type-safe, pragmatic, and high performance +library for frictionless and practical functional programming in Python 3.9+. + +By pragmatic, we mean that the goal of the library is to use simple abstractions +to enable you to do practical and productive functional programming in Python +(instead of being a [Monad tutorial](https://github.com/dbrattli/OSlash)). + +Python is a multi-paradigm programming language that also supports functional +programming constructs such as functions, higher-order functions, lambdas, and +in many ways favors composition over inheritance. + +> Better Python with F# + +Expression tries to make a better Python by providing several functional +features inspired by [F#](https://fsharp.org). This serves several +purposes: + +- Enable functional programming in a Pythonic way, i.e., make sure we are not + over-abstracting things. Expression will not require purely functional + programming as would a language like Haskell. +- Everything you learn with Expression can also be used with F#. Learn F# by + starting in a programming language they already know. Perhaps get inspired to + also [try out F#](https://aka.ms/fsharphome) by itself. +- Make it easier for F# developers to use Python when needed, and re-use many + of the concepts and abstractions they already know and love. + +Expression will enable you to work with Python using many of the same +programming concepts and abstractions. This enables concepts such as [Railway +oriented programming](https://fsharpforfunandprofit.com/rop/) (ROP) for better +and predictable error handling. Pipelining for workflows, computational +expressions, etc. + +> _Expressions evaluate to a value. Statements do something._ + +F# is a functional programming language for .NET that is succinct (concise, +readable, and type-safe) and kind of +[Pythonic](https://docs.python.org/3/glossary.html). F# is in many ways very +similar to Python, but F# can also do a lot of things better than Python: + +- Strongly typed, if it compiles it usually works making refactoring much + safer. You can trust the type-system. With [mypy](http://mypy-lang.org/) or + [Pylance](https://github.com/microsoft/pylance-release) you often wonder who + is right and who is wrong. +- Type inference, the compiler deduces types during compilation +- Expression based language + +## Getting Started + +You can install the latest `expression` from PyPI by running `pip` (or +`pip3`). Note that `expression` only works for Python 3.9+. + +```console +> pip3 install expression +``` + +## Goals + +- Industrial strength library for functional programming in Python. +- The resulting code should look and feel like Python + ([PEP-8](https://www.python.org/dev/peps/pep-0008/)). We want to make a + better Python, not some obscure DSL or academic Monad tutorial. +- Provide pipelining and pipe friendly methods. Compose all the things! +- Dot-chaining on objects as an alternative syntax to pipes. +- Lower the cognitive load on the programmer by: + - Avoid currying, not supported in Python by default and not a well known + concept by Python programmers. + - Avoid operator (`|`, `>>`, etc) overloading, this usually confuses more + than it helps. + - Avoid recursion. Recursion is not normally used in Python and any use of it + should be hidden within the SDK. +- Provide [type-hints](https://docs.python.org/3/library/typing.html) for all + functions and methods. +- Support PEP 634 and structural pattern matching. +- Code must pass strict static type checking by + [pylance](https://devblogs.microsoft.com/python/announcing-pylance-fast-feature-rich-language-support-for-python-in-visual-studio-code/). + Pylance is awesome, use it! +- [Pydantic](https://pydantic-docs.helpmanual.io/) friendly data types. Use Expression + types as part of your Pydantic data model and (de)serialize to/from JSON. + +## Supported features + +Expression will never provide you with all the features of F# and .NET. We are +providing a few of the features we think are useful, and will add more +on-demand as we go along. + +- **Pipelining** - for creating workflows. +- **Composition** - for composing and creating new operators. +- **Fluent or Functional** syntax, i.e., dot chain or pipeline operators. +- **Pattern Matching** - an alternative flow control to `if-elif-else`. +- **Error Handling** - Several error handling types. + - **Option** - for optional stuff and better `None` handling. + - **Result** - for better error handling and enables railway-oriented + programming in Python. + - **Try** - a simpler result type that pins the error to an Exception. +- **Collections** - immutable collections. + - **TypedArray** - a generic array type that abstracts the details of + `bytearray`, `array.array` and `list` modules. + - **Sequence** - a better + [itertools](https://docs.python.org/3/library/itertools.html) and + fully compatible with Python iterables. + - **Block** - a frozen and immutable list type. + - **Map** - a frozen and immutable dictionary type. + - **AsyncSeq** - Asynchronous iterables. + - **AsyncObservable** - Asynchronous observables. Provided separately + by [aioreactive](https://github.com/dbrattli/aioreactive). +- **Data Modelling** - sum and product types + - **TaggedUnion** - A tagged (discriminated) union type. +- **Parser Combinators** - A recursive decent string parser combinator + library. +- **Effects**: - lightweight computational expressions for Python. This + is amazing stuff. + - **option** - an optional world for working with optional values. + - **result** - an error handling world for working with result values. +- **Mailbox Processor**: for lock free programming using the [Actor + model](https://en.wikipedia.org/wiki/Actor_model). +- **Cancellation Token**: for cancellation of asynchronous (and + synchronous) workflows. +- **Disposable**: For resource management. + +### Pipelining + +Expression provides a `pipe` function similar to `|>` in F#. We don't want to +overload any Python operators, e.g., `|` so `pipe` is a plain old function taking +N-arguments, and will let you pipe a value through any number of functions. + +```python +from expression import pipe + +v = 1 +fn = lambda x: x + 1 +gn = lambda x: x * 2 + +assert pipe(v, fn, gn) == gn(fn(v)) +``` + +Expression objects (e.g., `Some`, `Seq`, `Result`) also have a `pipe` method, so you can dot chain pipelines +directly on the object: + +```python +from expression import Some + +v = Some(1) +fn = lambda x: x.map(lambda y: y + 1) +gn = lambda x: x.map(lambda y: y * 2) + +assert v.pipe(fn, gn) == gn(fn(v)) +``` + +So for example with sequences you may create sequence transforming +pipelines: + +```python +from expression.collections import seq, Seq + +xs = Seq.of(9, 10, 11) +ys = xs.pipe( + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0) +) + +assert ys == 110 +``` + +### Composition + +Functions may even be composed directly into custom operators: + +```python +from expression import compose +from expression.collections import seq, Seq + +xs = Seq.of(9, 10, 11) +custom = compose( + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0) +) +ys = custom(xs) + +assert ys == 110 +``` + +### Fluent and Functional + +Expression can be used both with a fluent or functional syntax (or both.) + +#### Fluent syntax + +The fluent syntax uses methods and is very compact. But it might get you into +trouble for large pipelines since it's not a natural way of adding line breaks. + +```python +from expression.collections import Seq + +xs = Seq.of(1, 2, 3) +ys = xs.map(lambda x: x * 100).filter(lambda x: x > 100).fold(lambda s, x: s + x, 0) +``` + +Note that fluent syntax is probably the better choice if you use mypy +for type checking since mypy may have problems inferring types through +larger pipelines. + +#### Functional syntax + +The functional syntax is a bit more verbose but you can easily add new +operations on new lines. The functional syntax is great to use together +with pylance/pyright. + +```python +from expression import pipe +from expression.collections import seq, Seq + +xs = Seq.of(1, 2, 3) +ys = pipe(xs, + seq.map(lambda x: x * 100), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0), +) +``` + +Both fluent and functional syntax may be mixed and even pipe can be used +fluently. + +```python +from expression.collections import seq, Seq +xs = Seq.of(1, 2, 3).pipe(seq.map(...)) +``` + +### Option + +The `Option` type is used when a function or method cannot produce a meaningful +output for a given input. + +An option value may have a value of a given type, i.e., `Some(value)`, or it might +not have any meaningful value, i.e., `Nothing`. + +```python +from expression import Some, Nothing, Option + +def keep_positive(a: int) -> Option[int]: + if a > 0: + return Some(a) + + return Nothing +``` + +```python +from expression import Option, Ok +def exists(x : Option[int]) -> bool: + match x: + case Some(_): + return True + return False +``` + +### Option as an effect + +Effects in Expression is implemented as specially decorated coroutines +([enhanced generators](https://www.python.org/dev/peps/pep-0342/)) using +`yield`, `yield from` and `return` to consume or generate optional values: + +```python +from expression import effect, Some + +@effect.option[int]() +def fn(): + x = yield 42 + y = yield from Some(43) + + return x + y + +xs = fn() +``` + +This enables ["railway oriented +programming"](https://fsharpforfunandprofit.com/rop/), e.g., if one part of the +function yields from `Nothing` then the function is side-tracked +(short-circuit) and the following statements will never be executed. The end +result of the expression will be `Nothing`. Thus results from such an option +decorated function can either be `Ok(value)` or `Error(error_value)`. + +```python +from expression import effect, Some, Nothing + +@effect.option[int]() +def fn(): + x = yield from Nothing # or a function returning Nothing + + # -- The rest of the function will never be executed -- + y = yield from Some(43) + + return x + y + +xs = fn() +assert xs is Nothing +``` + +For more information about options: + +- [Tutorial](https://expression.readthedocs.io/en/latest/tutorial/optional_values.html) +- [API reference](https://expression.readthedocs.io/en/latest/reference/option.html) + +### Result + +The `Result[T, TError]` type lets you write error-tolerant code that can be +composed. A Result works similar to `Option`, but lets you define the value used +for errors, e.g., an exception type or similar. This is great when you want to +know why some operation failed (not just `Nothing`). This type serves the same +purpose of an `Either` type where `Left` is used for the error condition and `Right` +for a success value. + +```python +from expression import effect, Ok, Result + +@effect.result[int, Exception]() +def fn(): + x = yield from Ok(42) + y = yield from Ok(10) + return x + y + +xs = fn() +assert isinstance(xs, Result) +``` + +A simplified type called `Try` is also available. It's a result type that is +pinned to `Exception` i.e., `Result[TSource, Exception]`. + +### Sequence + +Sequences is a thin wrapper on top of iterables and contains operations for working with +Python iterables. Iterables are immutable by design, and perfectly suited for functional +programming. + +```python +import functools +from expression import pipe +from expression.collections import seq + +# Normal python way. Nested functions are hard to read since you need to +# start reading from the end of the expression. +xs = range(100) +ys = functools.reduce(lambda s, x: s + x, filter(lambda x: x > 100, map(lambda x: x * 10, xs)), 0) + +# With Expression, you pipe the result, so it flows from one operator to the next: +zs = pipe( + xs, + seq.map(lambda x: x * 10), + seq.filter(lambda x: x > 100), + seq.fold(lambda s, x: s + x, 0), +) +assert ys == zs +``` + +## Tagged Unions + +Tagged Unions (aka discriminated unions) may look similar to normal Python Unions. But +they are [different](https://stackoverflow.com/a/61646841) in that the operands in a +type union `(A | B)` are both types, while the cases in a tagged union type `U = A | B` +are both constructors for the type U and are not types themselves. One consequence is +that tagged unions can be nested in a way union types might not. + +In Expression you make a tagged union by defining your type as a sub-class of +`TaggedUnion` with the appropriate generic types that this union represent for +each case. Then you define static or class-method constructors for creating each of the +tagged union cases. + +```python +from dataclasses import dataclass +from expression import TaggedUnion, tag + +@dataclass +class Rectangle: + width: float + length: float + +@dataclass +class Circle: + radius: float + +class Shape(TaggedUnion): + RECTANGLE = tag(Rectangle) + CIRCLE = tag(Circle) + + @staticmethod + def rectangle(width: float, length: float) -> Shape: + return Shape(Shape.RECTANGLE, Rectangle(width, length)) + + @staticmethod + def circle(radius: float) -> Shape: + return Shape(Shape.CIRCLE, Circle(radius)) +``` + +Now you may pattern match the shape to get back the actual value: + +```python + from expression import match + + shape = Shape.Rectangle(2.3, 3.3) + + match shape: + case Shape(value=Rectangle(width=2.3)): + assert shape.value.width == 2.3 + case _: + assert False +``` + +## Notable differences between Expression and F# + +In F# modules are capitalized, in Python they are lowercase +([PEP-8](https://www.python.org/dev/peps/pep-0008/#package-and-module-names)). +E.g in F# `Option` is both a module (`OptionModule` internally) and a +type. In Python the module is `option` and the type is capitalized i.e +`Option`. + +Thus in Expression you use `option` as the module to access module functions +such as `option.map` and the name `Option` for the type itself. + +```pycon +>>> from expression import Option, option +>>> Option +<class 'expression.core.option.Option'> +>>> option +<module 'expression.core.option' from '/Users/dbrattli/Developer/Github/Expression/expression/core/option.py'> +``` + +## Common Gotchas and Pitfalls + +A list of common problems and how you may solve it: + +### Expression is missing the function/operator I need + +Remember that everything is just a function, so you can easily implement +a custom function yourself and use it with Expression. If you think the +function is also usable for others, then please open a PR to include it +with Expression. + +## Resources and References + +A collection of resources that were used as reference and inspiration +for creating this library. + +- F# (http://fsharp.org) +- Get Started with F# (https://aka.ms/fsharphome) +- F# as a Better Python - Phillip Carter - NDC Oslo 2020 + (https://www.youtube.com/watch?v=_QnbV6CAWXc) +- OSlash (https://github.com/dbrattli/OSlash) +- RxPY (https://github.com/ReactiveX/RxPY) +- PEP 8 -- Style Guide for Python Code (https://www.python.org/dev/peps/pep-0008/) +- PEP 342 -- Coroutines via Enhanced Generators + (https://www.python.org/dev/peps/pep-0342/) +- PEP 380 -- Syntax for Delegating to a Subgenerator + (https://www.python.org/dev/peps/pep-0380) +- PEP 479 -- Change StopIteration handling inside generators (https://www.python.org/dev/peps/pep-0479/) +- PEP 634 -- Structural Pattern Matching (https://www.python.org/dev/peps/pep-0634/) +- Thunks, Trampolines and Continuation Passing + (https://jtauber.com/blog/2008/03/30/thunks,_trampolines_and_continuation_passing/) +- Tail Recursion Elimination + (http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html) +- Final Words on Tail Calls + (http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html) +- Python is the Haskell You Never Knew You Had: Tail Call Optimization + (https://sagnibak.github.io/blog/python-is-haskell-tail-recursion/) + +## How-to Contribute + +You are very welcome to contribute with suggestions or PRs :heart_eyes: It is +nice if you can try to align the code and naming with F# modules, functions, +and documentation if possible. But submit a PR even if you should feel unsure. + +Code, doc-strings, and comments should also follow the [Google Python Style +Guide](https://google.github.io/styleguide/pyguide.html). + +Code checks are done using + +- [Black](https://github.com/psf/black) +- [flake8](https://github.com/PyCQA/flake8) +- [isort](https://github.com/PyCQA/isort) + +To run code checks on changed files every time you commit, install the pre-commit hooks +by running: + +```console +> pre-commit install +``` + +## Code of Conduct + +This project follows https://www.contributor-covenant.org, see our [Code +of +Conduct](https://github.com/cognitedata/Expression/blob/main/CODE_OF_CONDUCT.md). + +## License + +MIT, see [LICENSE](https://github.com/cognitedata/Expression/blob/main/LICENSE). + + +%prep +%autosetup -n expression-4.2.4 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-expression -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Fri May 05 2023 Python_Bot <Python_Bot@openeuler.org> - 4.2.4-1 +- Package Spec generated @@ -0,0 +1 @@ +cc19bb9208f42c20f364b42ccc49e6d3 expression-4.2.4.tar.gz |