├── .github
└── workflows
│ └── python-publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── linqit
├── __init__.py
└── linq_list.py
├── setup.py
└── tests
├── __init__.py
└── test_list.py
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.x'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: __token__
39 | password: ${{ secrets.PYPI_API_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 |
103 | .idea
104 | .vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Linqit!
2 | Extends python's list builtin with fun, robust functionality - .NET's Language Integrated Queries (Linq) and more.
3 | Write clean code with powerful syntax.
4 | ```shell script
5 | pip install linqit
6 | ```
7 |
8 |
9 |
10 |
11 |
12 |
13 | Stop using loops, complex conditions, list comprehension and filters.
14 | Doesn't it looks better?
15 | ```python
16 | from linqit import List
17 |
18 | # Go ahead and fill the list with whatever you want... like a list of objects.
19 | programmers = List()
20 | Avi = type("Avi", (), {})
21 | Entrepreneur = type("Entrepreneur", ('talented'), {})
22 | elon_musk = Entrepreneur(talented=True)
23 |
24 | # Then play:
25 | last_hot_pizza_slice = (
26 | programmers.where(lambda e: e.experience > 15)
27 | .except_for(elon_musk)
28 | .of_type(Avi)
29 | .take(3) # [, , ]
30 | .select(lambda avi: avi.lunch) # [, , ]
31 | .where(lambda p: p.is_hot() and p.origin != "Pizza Hut")
32 | .last() #
33 | .slices.last() #
34 | )
35 |
36 | # What do you think?
37 | ```
38 |
39 | We all use multiple aggregations in our code, while multiple filters/comprehensions are not pythonic at all.
40 | The whole idea is is to use it for nested, multiple filters/modifications :).
41 | Some of the methods might look ridiculous for a single calls, comparing to the regular python syntax.
42 | Here are some use cases:
43 |
44 | #### Methods:
45 | ```
46 | all
47 | any
48 | concat
49 | contains
50 | distinct
51 | except_for
52 | first
53 | get_by_attr
54 | intersect
55 | last
56 | select
57 | skip
58 | take
59 | where
60 | of_type
61 | ```
62 | #### Properties:
63 | ```
64 | sum
65 | min
66 | max
67 | avg
68 | sorted
69 | ```
70 |
71 | ## Deeper - Let's play with a list of people, a custom type.
72 | ```python
73 | import List
74 |
75 | class Person():
76 | def __init__(self, name, age):
77 | self.name = name
78 | self.age = age
79 |
80 | def __repr__(self):
81 | return f'Person(name="{self.name}", age={self.age})')
82 |
83 |
84 | # Creating a list of people
85 | avi, bill, bob, harry = Person('Avi', 23), Person('Bill', 41), Person('Bob', 77), Person('Harry', 55)
86 |
87 | people = List(avi, bill, bob, harry)
88 | ```
89 |
90 | ## Use LINQ selections, write cleaner code
91 | ```python
92 | people = people.where(lambda p: p.age > 23) # [, , ]
93 | people.first() #
94 | people.last() #
95 | people.any(lambda p: p.name.lower().startswith('b')) # True
96 | people.where(age=55) # []
97 | people.skip(3).any() # False
98 | people.skip(2).first() #
99 |
100 | # Isn't it better than "for", "if", "else", "filter", "map" and list comprehensions in the middle of your code?
101 |
102 | ```
103 | ## More selections
104 | ```python
105 | new_kids_in_town = [Person('Chris', 18), Person('Danny', 16), Person('John', 17)]
106 | people += new_kids_in_town # Also works: people = people.concat(new_kids_in_town)
107 |
108 | teenagers = people.where(lambda p: 20 >= p.age >= 13)
109 | danny = teenagers.first(lambda t: t.name == 'Danny') #
110 | oldest_teen = teenagers.order_by(lambda t: t.age).last() #
111 | ```
112 |
113 | ## Let's make python more dynamic
114 | ```python
115 | names = people.name # ['Avi', 'Bill', 'Bob', 'Harry', 'Chris', 'John']
116 | ages = people.age # [23, 41, 77, 55, 18, 17]
117 | teenagers_names = teenagers.name # ['Chris', 'Danny', 'John']
118 | teenagers_names.take(2).except_for(lambda n: n == 'Danny') # ['Chris']
119 | teenagers.age.min # 16
120 | teenagers.age.avg # 17
121 | teenagers.age.max # 18
122 | ```
123 |
124 | # Test Coverage
125 | ```python
126 | ➜ linqit git:(master) ✗ coverage report
127 | Name Stmts Miss Cover
128 | -----------------------------------------
129 | linqit/__init__.py 2 0 100%
130 | linqit/linq_list.py 101 11 89%
131 | tests/__init__.py 0 0 100%
132 | tests/test_list.py 203 0 100%
133 | -----------------------------------------
134 | TOTAL 306 11 96%
135 | ```
136 |
--------------------------------------------------------------------------------
/linqit/__init__.py:
--------------------------------------------------------------------------------
1 | from linqit.linq_list import (
2 | List,
3 | )
4 |
5 | __all__ = ["List"]
6 |
--------------------------------------------------------------------------------
/linqit/linq_list.py:
--------------------------------------------------------------------------------
1 | from functools import (
2 | reduce,
3 | )
4 | from itertools import (
5 | chain,
6 | )
7 |
8 | _DEFAULT_LAZY = True
9 | _NO_EXPR = lambda x:x
10 |
11 | # A default variable for the function, so None as an argument will be valid, but not default.
12 | _NONE = type('_NONE', (object,), {})
13 |
14 | class List(list):
15 | """
16 | Extends Python's built-in list with additional functionality.
17 | Inspired by .NET's Language Integrated Queries (LINQ).
18 | This class wraps an iterable of objects.
19 | It enables them to be manipulated as a single entity.
20 | """
21 |
22 | def __init__(self, *objects):
23 | """
24 | Initializes a new List instance.
25 |
26 | :param objects: An iterable of objects to be contained.
27 | :type objects: iterable
28 | """
29 | super(List, self,).__init__(
30 | objects[0]
31 | if len(objects) == 1
32 | and hasattr(
33 | objects[0],
34 | "__iter__",
35 | )
36 | else objects
37 | )
38 |
39 | def __getitem__(self, item):
40 | """
41 | Retrieves the item(s) from the list at the specified index or slice.
42 |
43 | :param item: Index or slice specifying the item(s) to retrieve.
44 | :type item: int or slice
45 | :return: The item(s) from the list.
46 | :rtype: List or object
47 | """
48 | _item = super(
49 | List,
50 | self,
51 | ).__getitem__(item)
52 |
53 | if isinstance(
54 | item,
55 | slice,
56 | ):
57 | _item = List(_item)
58 | return _item
59 |
60 | def __getattr__(self, item):
61 | """
62 | Retrieves the attribute(s) with the specified name from the objects in the list.
63 |
64 | :param item: The attribute to retrieve.
65 | :type item: str
66 | :return: The attribute(s) from the objects in the list.
67 | :rtype: List or object
68 | :raises AttributeError: If the attribute does not exist in any object.
69 | """
70 | relevant_objects = list(
71 | filter(
72 | lambda obj: hasattr(
73 | obj,
74 | item,
75 | ),
76 | self,
77 | )
78 | )
79 | if not relevant_objects:
80 | raise AttributeError(item)
81 |
82 | attributes_values = List(
83 | [
84 | getattr(
85 | obj,
86 | item,
87 | )
88 | for obj in relevant_objects
89 | ]
90 | )
91 |
92 | if all(
93 | hasattr(
94 | attr_value,
95 | "__iter__",
96 | )
97 | and not isinstance(
98 | attr_value,
99 | str,
100 | )
101 | for attr_value in attributes_values
102 | ):
103 | return List(chain.from_iterable(attributes_values))
104 | elif all(callable(attr_value) for attr_value in attributes_values):
105 | return lambda *args: List(
106 | [
107 | attr_value(*args)
108 | for obj, attr_value in zip(
109 | relevant_objects,
110 | attributes_values,
111 | )
112 | ]
113 | )
114 |
115 | return attributes_values
116 |
117 | def __add__(self, other):
118 | """
119 | Concatenates this list with another list or iterable.
120 |
121 | :param other: The list or iterable to concatenate.
122 | :type other: list or iterable
123 | :return: A new List containing the concatenated items.
124 | :rtype: List
125 | """
126 | added_list = super(
127 | List,
128 | self,
129 | ).__add__(other)
130 | return List(added_list)
131 |
132 | def __mul__(self, other):
133 | """
134 | Multiplies this list by an integer value.
135 |
136 | :param other: The integer value to multiply the list.
137 | :type other: int
138 | :return: A new List containing the multiplied items.
139 | :rtype: List
140 | """
141 | multi_list = super(
142 | List,
143 | self,
144 | ).__mul__(other)
145 | return List(multi_list)
146 |
147 | @property
148 | def sum(self):
149 | """
150 | Calculates the sum of all the values in the list.
151 |
152 | :return: The sum of the values.
153 | :rtype: object
154 | """
155 | return sum(self)
156 |
157 | @property
158 | def min(self):
159 | """
160 | Finds the lowest value in the list.
161 |
162 | :return: The lowest value.
163 | :rtype: object
164 | """
165 | return min(self)
166 |
167 | @property
168 | def max(self):
169 | """
170 | Finds the highest value in the list.
171 |
172 | :return: The highest value.
173 | :rtype: object
174 | """
175 | return max(self)
176 |
177 | @property
178 | def avg(self):
179 | """
180 | Calculates the average of the numerical values in the list.
181 | :return: The average of the values.
182 | :rtype: float
183 | """
184 | return reduce(
185 | lambda x, y: x + y,
186 | self,
187 | ) / len(self)
188 |
189 | @property
190 | def sorted(
191 | self,
192 | ):
193 | """
194 | Returns a new sorted list of the elements.
195 |
196 | :return: A new sorted list.
197 | :rtype: List
198 | """
199 | return sorted(self)
200 |
201 | def all(
202 | self,
203 | expression=_NO_EXPR,
204 | ):
205 | """
206 | Checks if all objects in the list satisfy the given expression.
207 |
208 | :param expression: The expression to evaluate for each object.
209 | :type expression: function
210 | :return: True if all objects satisfy the expression, False otherwise.
211 | :rtype: bool
212 | """
213 | if self:
214 | for i in self:
215 | if not expression(i):
216 | return False
217 | return True
218 |
219 |
220 | def any(
221 | self,
222 | expression=_NO_EXPR,
223 | ):
224 | """
225 | Checks if any object in the list satisfies the given expression.
226 |
227 | :param expression: The expression to evaluate for each object.
228 | :type expression: function
229 | :return: True if any object satisfies the expression, False otherwise.
230 | :rtype: bool
231 | """
232 | if self:
233 | for i in self:
234 | if expression(i):
235 | return True
236 | return False # for an empty iterable, all returns False!
237 |
238 | def concat(self, second):
239 | """
240 | Concatenates this list with another list or iterable.
241 |
242 | :param second: The list or iterable to concatenate.
243 | :type second: list or iterable
244 | :return: A new List containing the concatenated items.
245 | :rtype: List
246 | """
247 | return List(self + second)
248 |
249 | def contains(self, item):
250 | """
251 | Checks if the list contains the specified item.
252 |
253 | :param item: The item to check for.
254 | :return: True if the item is in the list, False otherwise.
255 | :rtype: bool
256 | """
257 | return item in self
258 |
259 | def distinct(
260 | self,
261 | ):
262 | """
263 | Returns a new list with no duplicate items.
264 |
265 | :return: A new list with distinct items.
266 | :rtype: List
267 | """
268 | return List(set(self))
269 |
270 | def except_for(
271 | self,
272 | expression,
273 | ):
274 | """
275 | Returns a new list containing the objects that do not satisfy the given expression.
276 |
277 | :param expression: The expression to evaluate for each object.
278 | :type expression: function
279 | :return: A new List containing the filtered objects.
280 | :rtype: List
281 | """
282 | return List(
283 | filter(
284 | lambda e: not expression(e),
285 | self,
286 | )
287 | )
288 |
289 | def first(
290 | self,
291 | expression=_NO_EXPR, default=_NONE
292 | ):
293 | """
294 | Returns the first object that satisfies the given expression, or the first element if no expression is provided.
295 |
296 | :param expression: The expression to evaluate for each object.
297 | :type expression: function or None
298 | :param default: The default value to return if no matching value is found.
299 | :return: The first matching object or the default value.
300 | :rtype: object
301 | :raises IndexError: If no matching value is found and no default value is provided.
302 | """
303 | if self:
304 | for el in self:
305 | if expression(el):
306 | return(el)
307 | if default != _NONE:
308 | return default
309 | else:
310 | raise IndexError('No matching values')
311 |
312 |
313 | def get_by_attr(self, attr):
314 | """
315 | Retrieves all objects in the list that have the specified attribute.
316 |
317 | :param attr: The attribute name.
318 | :type attr: str
319 | :return: A new List containing the objects with the attribute.
320 | :rtype: List
321 | """
322 | try:
323 | return getattr(
324 | self,
325 | attr,
326 | )
327 | except AttributeError:
328 | return List()
329 |
330 | def intersect(self, second):
331 | """
332 | Returns a new list containing the objects that are present in both this list and the second list.
333 |
334 | :param second: The list or iterable to intersect with.
335 | :type second: list or iterable
336 | :return: A new List containing the intersecting objects.
337 | :rtype: List
338 | """
339 | return List(
340 | filter(
341 | lambda e: e in second,
342 | self,
343 | )
344 | )
345 |
346 | def last(
347 | self,
348 | expression=_NO_EXPR,
349 | default=_NONE,
350 | ):
351 | """
352 | Returns the last object that satisfies the given expression, or the last element if no expression is provided.
353 |
354 | :param expression: The expression to evaluate for each object.
355 | :type expression: function or None
356 | :param default: The default value to return if no matching value is found.
357 | :return: The last matching object or the default value.
358 | :rtype: object
359 | :raises IndexError: If no matching value is found and no default value is provided.
360 | """
361 | return List(reversed(self)).first(
362 | expression,
363 | default,
364 | )
365 |
366 | def order_by(
367 | self,
368 | expression=None,
369 | ):
370 | """
371 | Returns a new list of objects sorted according to the provided expression.
372 | If no expression is given, the default sort is used.
373 |
374 | :param expression: The expression to determine the sorting order.
375 | :type expression: function or None
376 | :return: A new sorted List.
377 | :rtype: List
378 | """
379 | sorted_data = sorted(
380 | self,
381 | key=expression,
382 | )
383 | return List(sorted_data)
384 |
385 | def select(
386 | self,
387 | expression,
388 | ):
389 | """
390 | Returns a new list containing the values obtained by applying the expression to each object in the list.
391 |
392 | :param expression: The expression to transform each object.
393 | :type expression: function
394 | :return: A new List containing the transformed values.
395 | :rtype: List
396 | """
397 | return List(
398 | map(
399 | expression,
400 | self,
401 | )
402 | )
403 |
404 | def skip(self, count):
405 | """
406 | Returns a new list containing the elements starting from the specified index.
407 |
408 | :param count: The number of elements to skip.
409 | :type count: int
410 | :return: A new List containing the remaining elements.
411 | :rtype: List
412 | """
413 | return List(self[count:])
414 |
415 | def take(self, count):
416 | """
417 | Returns a new list containing the first n elements.
418 |
419 | :param count: The number of elements to take.
420 | :type count: int
421 | :return: A new List containing the first n elements.
422 | :rtype: List
423 | """
424 | if not self or count == 0:
425 | return List()
426 | return self[:count]
427 |
428 | def where(self, expression=None, **filters):
429 | """
430 | Returns a new list containing the objects that satisfy the given expression and filters.
431 |
432 | :param expression: The expression to evaluate for each object.
433 | :type expression: function or None
434 | :param filters: Additional attribute filters to apply.
435 | :type filters: dict
436 | :return: A new List containing the filtered objects.
437 | :rtype: List
438 | """
439 |
440 | def filter_function(
441 | x,
442 | ):
443 | return (expression is None or expression(x)) and all(
444 | [
445 | getattr(
446 | x,
447 | key,
448 | )
449 | == value
450 | for key, value in filters.items()
451 | ]
452 | )
453 |
454 | selection = filter(
455 | filter_function,
456 | self,
457 | )
458 | return List(selection)
459 |
460 | def of_type(self, _type):
461 | """
462 | Returns a new list containing the objects of the specified type.
463 |
464 | :param _type: The type of objects to filter.
465 | :type _type: type
466 | :return: A new List containing the filtered objects.
467 | :rtype: List
468 | :raises TypeError: If the argument is not a type.
469 | """
470 | if not isinstance(
471 | _type,
472 | type,
473 | ):
474 | raise TypeError("The argument must be a type")
475 | return self.where(
476 | lambda e: isinstance(
477 | e,
478 | _type,
479 | )
480 | )
481 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="linqit",
8 | version="0.1.5",
9 | author="Avi Lumelsky",
10 | author_email="noticetheg@gmail.com",
11 | description="Extends python's list builtin with fun, robust functionality - "
12 | ".NET's Language Integrated Queries (Linq) and more. Write clean code with powerful syntax.",
13 | long_description=long_description,
14 | long_description_content_type="text/markdown",
15 | url="https://github.com/avilum/linqit",
16 | packages=setuptools.find_packages(),
17 | classifiers=[
18 | "Programming Language :: Python :: 2",
19 | "Programming Language :: Python :: 3",
20 | "License :: OSI Approved :: Apache Software License",
21 | "Operating System :: OS Independent",
22 | ],
23 | python_requires=">=2.7,>=3.6",
24 | )
25 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/avilum/linqit/1eb357cf215b5382fdb3b82c6d4af286d5aebc17/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_list.py:
--------------------------------------------------------------------------------
1 | from datetime import (
2 | datetime,
3 | )
4 | from unittest import (
5 | TestCase,
6 | )
7 |
8 | from linqit import (
9 | List,
10 | )
11 |
12 |
13 | class Mock(object):
14 | pass
15 |
16 |
17 | counter = 0
18 |
19 |
20 | def func_samson(x):
21 | global counter
22 | counter += 1
23 | return x.last_name == "samson"
24 |
25 |
26 | def func_lee(x):
27 | global counter
28 | counter += 1
29 | return x.last_name == "lee"
30 |
31 |
32 | class Person(object):
33 | def __init__(
34 | self,
35 | first_name,
36 | last_name,
37 | hobby,
38 | age,
39 | ):
40 | self.first_name = first_name
41 | self.last_name = last_name
42 | self.hobby = hobby
43 | self.age = age
44 | self.full_name = " ".join(
45 | [
46 | self.first_name,
47 | self.last_name,
48 | ]
49 | )
50 |
51 | def do_something(
52 | self,
53 | ):
54 | return "{name} is {what}.".format(
55 | name=self.full_name,
56 | what=self.hobby.lower(),
57 | )
58 |
59 |
60 | class ListTests(TestCase):
61 | """
62 | UnitTests (edge cases tests) of the List class
63 | """
64 |
65 | def setUp(self):
66 | self.list = self._get_dynamic_list_with_mocks()
67 |
68 | def _get_dynamic_list_with_mocks(
69 | self,
70 | ):
71 | a, b = (
72 | Mock(),
73 | Mock(),
74 | )
75 | a.name = "bob"
76 | a.age = 20
77 | b.name = "john"
78 | b.age = 20
79 | return List(a, b)
80 |
81 | # Sanity and edge cases tests
82 |
83 | def test_get_list_properties_should_combine_values(
84 | self,
85 | ):
86 | a, b = (
87 | Mock(),
88 | Mock(),
89 | )
90 | a.phones = [
91 | 1,
92 | 2,
93 | 123321,
94 | 4,
95 | ]
96 | b.phones = [
97 | 1,
98 | 2,
99 | 5,
100 | 789,
101 | ]
102 | self.list = List(a, b)
103 | self.assertEqual(
104 | self.list.phones.sort(),
105 | [
106 | 1,
107 | 2,
108 | 123321,
109 | 4,
110 | 1,
111 | 2,
112 | 5,
113 | 789,
114 | ].sort(),
115 | )
116 | self.assertEqual(
117 | len(self.list.phones),
118 | 8,
119 | )
120 |
121 | def test_non_list_properties_should_combine_values(
122 | self,
123 | ):
124 | a, b = (
125 | Mock(),
126 | Mock(),
127 | )
128 | a.foo = 1
129 | b.foo = 2
130 | self.list = List(a, b)
131 | self.assertEqual(
132 | self.list.foo,
133 | [1, 2],
134 | )
135 |
136 | def test_non_list_peoperties_with_unique_object_peoperty(
137 | self,
138 | ):
139 | a, b = (
140 | Mock(),
141 | Mock(),
142 | )
143 | a.foo = 1
144 | b.bar = 2
145 | self.list = List(a, b)
146 | self.assertEqual(
147 | self.list.foo,
148 | [1],
149 | )
150 | self.assertEqual(
151 | self.list.bar,
152 | [2],
153 | )
154 |
155 | def test_list_peoperties_with_unique_object_peoperty(
156 | self,
157 | ):
158 | a, b = (
159 | Mock(),
160 | Mock(),
161 | )
162 | a.foo = [1]
163 | b.bar = [2]
164 | self.list = List(a, b)
165 | self.assertEqual(
166 | self.list.foo,
167 | [1],
168 | )
169 | self.assertEqual(
170 | self.list.bar,
171 | [2],
172 | )
173 |
174 | def test_init_with_several_items(
175 | self,
176 | ):
177 | dynamic_list = List(
178 | Mock(),
179 | Mock(),
180 | Mock(),
181 | )
182 | self.assertTrue(dynamic_list)
183 |
184 | def test_different_types_sanity(
185 | self,
186 | ):
187 | a, b = (
188 | Mock(),
189 | Mock(),
190 | )
191 | a.foo = "a"
192 | b.foo = 1
193 | self.list = List(a, b)
194 | self.assertTrue("a" in self.list.foo)
195 | self.assertTrue(1 in self.list.foo)
196 | self.assertTrue(len(self.list.foo) == 2)
197 |
198 | def test_runtime_changes_apply(
199 | self,
200 | ):
201 | john = Person(
202 | "John",
203 | "Doe",
204 | "Coding",
205 | 27,
206 | )
207 | bob = Person(
208 | "Bob",
209 | "Marley",
210 | "Playing guitar",
211 | 33,
212 | )
213 |
214 | # Creating a dynamic_list
215 | dynamic_list = List(
216 | john,
217 | bob,
218 | )
219 |
220 | # Powerful functionality
221 | dynamic_list.first_name # ['John', 'Bob']
222 | dynamic_list.full_name # ['John Doe', 'Bob Marley']
223 | dynamic_list.do_something() # ['John Doe is coding', 'Bob Marley is playing guitar']
224 |
225 | # Dynamic Runtime changes:
226 | bob.birthday = datetime(
227 | year=1945,
228 | month=2,
229 | day=6,
230 | )
231 | dynamic_list.birthday # datetime.datetime(1945, 2, 6, 0, 0)
232 | john.birthday = datetime(
233 | year=1970,
234 | month=1,
235 | day=1,
236 | )
237 | dynamic_list.birthday # : [datetime.datetime(1970, 1, 1, 0, 0), datetime.datetime(1945, 2, 6, 0, 0)]
238 | self.assertTrue(len(dynamic_list.birthday) == 2)
239 |
240 | def test_add_method(
241 | self,
242 | ):
243 | added_list = self.list + self.list
244 | self.assertTrue(len(added_list) == 4)
245 | self.assertTrue(
246 | isinstance(
247 | added_list,
248 | List,
249 | )
250 | )
251 |
252 | def test_times_method(
253 | self,
254 | ):
255 | timed_list = self.list * 3
256 | self.assertTrue(len(timed_list) == 6)
257 | self.assertTrue(
258 | isinstance(
259 | timed_list,
260 | List,
261 | )
262 | )
263 |
264 | def test_slice_method(
265 | self,
266 | ):
267 | sliced_list = self.list[:-1]
268 | self.assertTrue(len(sliced_list) == 1)
269 | self.assertTrue(
270 | isinstance(
271 | sliced_list,
272 | List,
273 | )
274 | )
275 |
276 | def test_non_existing_attribute_raises_attribute_error(
277 | self,
278 | ):
279 | self.assertRaises(
280 | AttributeError,
281 | self.list.__getattribute__,
282 | "blabla",
283 | )
284 |
285 | # LINQ Methods tests
286 |
287 | def test_where_method(
288 | self,
289 | ):
290 | self.assertEqual(
291 | self.list.where(lambda p: p.name == "bob")[0],
292 | self.list[0],
293 | )
294 |
295 | def test_where_method_without_expression(
296 | self,
297 | ):
298 | self.assertEqual(
299 | self.list.where(),
300 | self.list,
301 | )
302 |
303 | def test_where_method_with_filter_kwargs(
304 | self,
305 | ):
306 | self.assertEqual(
307 | self.list.where(name="bob")[0],
308 | self.list[0],
309 | )
310 |
311 | def test_where_method_with_expression_and_filter_kwargs(
312 | self,
313 | ):
314 | self.assertEqual(
315 | self.list.where(
316 | lambda x: len(x.name) == 4,
317 | age=20,
318 | )[0],
319 | self.list[1],
320 | )
321 |
322 | def test_where_method_with_multiple_filter_kwargs(
323 | self,
324 | ):
325 | self.assertEqual(
326 | self.list.where(
327 | name="bob",
328 | age=15,
329 | ),
330 | [],
331 | )
332 |
333 | def test_first_method(
334 | self,
335 | ):
336 | self.assertEqual(
337 | self.list.first(lambda p: p.name == "bob"),
338 | self.list[0],
339 | )
340 |
341 | def test_first_method_with_nonexisisting_value_raises_indexerror(
342 | self,
343 | ):
344 | self.assertRaises(
345 | IndexError,
346 | self.list.first,
347 | lambda p: p.name == "id that dont exist",
348 | )
349 |
350 | def test_first_method_with_nonexisisting_value_returns_none(
351 | self,
352 | ):
353 | self.assertEqual(
354 | None,
355 | self.list.first(
356 | lambda p: p.name == "danny",
357 | None,
358 | ),
359 | )
360 |
361 | def test_any_method(
362 | self,
363 | ):
364 | self.assertEqual(
365 | True,
366 | self.list.any(lambda p: p.name == "bob"),
367 | )
368 |
369 | def test_any_method_with_all_false(
370 | self,
371 | ):
372 | self.assertEqual(
373 | False,
374 | self.list.any(lambda p: p.name == "danny"),
375 | )
376 |
377 | def test_any_method_without_expression(
378 | self,
379 | ):
380 | self.assertEqual(
381 | True,
382 | self.list.any(),
383 | )
384 |
385 | def test_any_method_without_expression_and_empty_list(
386 | self,
387 | ):
388 | empty = List()
389 | self.assertEqual(
390 | False,
391 | empty.any(),
392 | )
393 |
394 | def test_all_method(
395 | self,
396 | ):
397 | self.assertEqual(
398 | True,
399 | self.list.all(lambda p: p.age == 20),
400 | )
401 |
402 | def test_all_method_with_false_arguments(
403 | self,
404 | ):
405 | self.assertEqual(
406 | False,
407 | self.list.all(lambda p: p.age == 19),
408 | )
409 |
410 | def test_contains_method_with_existing_item(
411 | self,
412 | ):
413 | self.assertTrue(self.list.contains(self.list[0]))
414 |
415 | def test_contains_method_with_non_existing_item(
416 | self,
417 | ):
418 | self.assertFalse(self.list.contains("123"))
419 |
420 | def test_sum_method(
421 | self,
422 | ):
423 | self.list = List(
424 | 1,
425 | 2,
426 | 3,
427 | 4,
428 | 5,
429 | )
430 | self.assertEqual(
431 | self.list.sum,
432 | 15,
433 | )
434 |
435 | def test_min_method(
436 | self,
437 | ):
438 | self.list = List(
439 | 1,
440 | 2,
441 | 3,
442 | 4,
443 | 5,
444 | )
445 | self.assertEqual(
446 | self.list.min,
447 | 1,
448 | )
449 |
450 | def test_max_method(
451 | self,
452 | ):
453 | self.list = List(
454 | 1,
455 | 2,
456 | 3,
457 | 4,
458 | 5,
459 | )
460 | self.assertEqual(
461 | self.list.max,
462 | 5,
463 | )
464 |
465 | def test_avg_method(
466 | self,
467 | ):
468 | self.list = List(
469 | 1,
470 | 2,
471 | 3,
472 | 4,
473 | 5,
474 | )
475 | self.assertEqual(
476 | self.list.avg,
477 | 3,
478 | )
479 |
480 | def test_select_method(
481 | self,
482 | ):
483 | def expression(
484 | e,
485 | ):
486 | return e.name
487 |
488 | self.assertEqual(
489 | self.list.select(expression),
490 | self.list.name,
491 | )
492 |
493 | def test_skip_method(
494 | self,
495 | ):
496 | self.assertEqual(
497 | self.list.skip(1)[0],
498 | self.list[1],
499 | )
500 |
501 | def test_take_method(
502 | self,
503 | ):
504 | self.list = self.list * 6
505 | taken_elements = self.list.take(2)
506 | self.assertEqual(
507 | taken_elements,
508 | self.list[:2],
509 | )
510 |
511 | def test_concat_method(
512 | self,
513 | ):
514 | m = Mock()
515 | m.name = "Avi"
516 | l2 = List(m)
517 | concatinated_list = self.list.concat(l2)
518 | self.assertEqual(
519 | len(concatinated_list),
520 | 3,
521 | )
522 | self.assertEqual(
523 | concatinated_list[-1],
524 | m,
525 | )
526 |
527 | def test_intersect_method(
528 | self,
529 | ):
530 | e1 = self.list[0]
531 | l2 = List(e1)
532 | intersected = self.list.intersect(l2)
533 | self.assertEqual(
534 | len(intersected),
535 | 1,
536 | )
537 | self.assertEqual(
538 | intersected[0],
539 | e1,
540 | )
541 |
542 | def test_except_for_method(
543 | self,
544 | ):
545 | excepted_list = self.list.except_for(lambda x: x.name.lower().startswith("b"))
546 | self.assertEqual(
547 | len(excepted_list),
548 | 1,
549 | )
550 | self.assertEqual(
551 | excepted_list[0],
552 | self.list[1],
553 | )
554 |
555 | def test_one_liner(
556 | self,
557 | ):
558 | self.assertEqual(
559 | 20,
560 | self.list.concat(self.list)
561 | .where(lambda e: e.name.lower() == "john")
562 | .skip(1)
563 | .select(lambda x: x.age)
564 | .avg,
565 | )
566 | self.list[1].age = 89
567 | self.list[1].name = "Bobby Brown"
568 | self.assertEqual(
569 | self.list[1].name,
570 | self.list.concat(self.list)
571 | .where(lambda e: e.age > 18)
572 | .skip(1)
573 | .except_for(lambda e: e.name == "bob")
574 | .select(lambda x: x.name)
575 | .last(),
576 | )
577 |
578 | def test_order_by_bare(
579 | self,
580 | ):
581 | data = [
582 | 1,
583 | -1,
584 | 7,
585 | 200,
586 | 4,
587 | 3,
588 | ]
589 | sorted_data = sorted(data)
590 |
591 | linq_data = List(data).order_by()
592 | self.assertEqual(
593 | sorted_data,
594 | linq_data,
595 | )
596 |
597 | def test_order_by_complex(
598 | self,
599 | ):
600 | data = [
601 | Person(
602 | "jake",
603 | "samson",
604 | None,
605 | 32,
606 | ),
607 | Person(
608 | "sam",
609 | "thompson",
610 | None,
611 | 44,
612 | ),
613 | Person(
614 | "sarah",
615 | "smith",
616 | None,
617 | 41,
618 | ),
619 | Person(
620 | "zoe",
621 | "lee",
622 | None,
623 | 27,
624 | ),
625 | ]
626 | sorted_data = sorted(
627 | data,
628 | key=lambda p: p.age,
629 | reverse=True,
630 | )
631 |
632 | linq_data = List(data).order_by(lambda p: -p.age)
633 | self.assertEqual(
634 | sorted_data,
635 | linq_data,
636 | )
637 |
638 | def test_any_method_short_circuit(
639 | self,
640 | ):
641 | data = [
642 | Person(
643 | "jake",
644 | "samson",
645 | None,
646 | 32,
647 | ),
648 | Person(
649 | "sam",
650 | "thompson",
651 | None,
652 | 44,
653 | ),
654 | Person(
655 | "sarah",
656 | "smith",
657 | None,
658 | 41,
659 | ),
660 | Person(
661 | "zoe",
662 | "lee",
663 | None,
664 | 27,
665 | ),
666 | ]
667 |
668 | global counter
669 | counter = 0
670 | linq_data = List(data).any(func_samson)
671 | self.assertEqual(
672 | linq_data,
673 | True,
674 | )
675 | self.assertEqual(
676 | counter,
677 | 1,
678 | )
679 |
680 | counter = 0
681 | linq_data = List(data).any(func_lee)
682 | self.assertEqual(
683 | linq_data,
684 | True,
685 | )
686 | self.assertEqual(
687 | counter,
688 | 4,
689 | )
690 |
691 | def test_all_method_short_circuit(
692 | self,
693 | ):
694 | data = [
695 | Person(
696 | "jake",
697 | "samson",
698 | None,
699 | 32,
700 | ),
701 | Person(
702 | "sam",
703 | "james",
704 | None,
705 | 44,
706 | ),
707 | Person(
708 | "sarah",
709 | "smith",
710 | None,
711 | 41,
712 | ),
713 | Person(
714 | "zoe",
715 | "lee",
716 | None,
717 | 27,
718 | ),
719 | ]
720 |
721 | global counter
722 | counter = 0
723 | linq_data = List(data).all(func_samson)
724 | self.assertEqual(
725 | linq_data,
726 | False,
727 | )
728 | self.assertEqual(
729 | counter,
730 | 2,
731 | ) # One true, then false
732 |
733 | data = [
734 | Person(
735 | "jake",
736 | "samson",
737 | None,
738 | 32,
739 | ),
740 | Person(
741 | "sam",
742 | "samson",
743 | None,
744 | 44,
745 | ),
746 | Person(
747 | "sarah",
748 | "samson",
749 | None,
750 | 41,
751 | ),
752 | Person(
753 | "zoe",
754 | "samson",
755 | None,
756 | 27,
757 | ),
758 | ]
759 |
760 | counter = 0
761 | linq_data = List(data).all(func_samson)
762 | self.assertEqual(
763 | linq_data,
764 | True,
765 | )
766 | self.assertEqual(
767 | counter,
768 | 4,
769 | ) # all 4 true
770 |
--------------------------------------------------------------------------------