├── .gitignore ├── LICENSE ├── README.rst ├── cnamedtuple ├── __init__.py └── _namedtuple.c ├── prof ├── bench ├── bench.out ├── instance_creation.png ├── type_creation_seq.png └── type_creation_string.png ├── setup.py └── tests └── test_namedtuple.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | cnamedtuple 0.1.6 2 | ================= 3 | 4 | An implementation of namedtuple written in c for warp speed. 5 | 6 | Tested against Python 3.4 and 3.5, and 3.6. 7 | 8 | 9 | Graphs 10 | `````` 11 | 12 | These operations scale with the number of fields. 13 | 14 | .. figure:: https://raw.githubusercontent.com/llllllllll/cnamedtuple/master/prof/type_creation_string.png 15 | :alt: Type creation from a string of field names. 16 | 17 | Type creation from a string of field names. 18 | .. figure:: https://raw.githubusercontent.com/llllllllll/cnamedtuple/master/prof/type_creation_seq.png 19 | :alt: Type creation from a sequence of field names. 20 | 21 | Type creation from a sequence of field names. 22 | .. figure:: https://raw.githubusercontent.com/llllllllll/cnamedtuple/master/prof/instance_creation.png 23 | :alt: Instance creation. 24 | 25 | Instance creation. 26 | 27 | Benchmarks 28 | ---------- 29 | 30 | .. code:: 31 | 32 | $ ./prof/bench 33 | Running with: Python 3.6.1 (default, Mar 27 2017, 00:27:06) [GCC 6.3.1 20170306] 34 | 35 | type creation from string with 1 field(s): namedtuple('NT', 'a') 36 | collections: 253 us +- 10 us 37 | cnamedtuple: 6.47 us +- 0.38 us 38 | ratio: 39.03 39 | 40 | type creation from string with 2 field(s): namedtuple('NT', 'a b') 41 | collections: 271 us +- 8 us 42 | cnamedtuple: 6.59 us +- 0.17 us 43 | ratio: 41.18 44 | 45 | type creation from string with 3 field(s): namedtuple('NT', 'a b c') 46 | collections: 290 us +- 6 us 47 | cnamedtuple: 6.77 us +- 0.17 us 48 | ratio: 42.80 49 | 50 | type creation from string with 4 field(s): namedtuple('NT', 'a b c d') 51 | collections: 308 us +- 12 us 52 | cnamedtuple: 7.07 us +- 0.21 us 53 | ratio: 43.62 54 | 55 | type creation from string with 5 field(s): namedtuple('NT', 'a b c d e') 56 | collections: 322 us +- 5 us 57 | cnamedtuple: 7.38 us +- 0.18 us 58 | ratio: 43.66 59 | 60 | type creation from string with 6 field(s): namedtuple('NT', 'a b c d e f') 61 | collections: 337 us +- 4 us 62 | cnamedtuple: 7.67 us +- 0.26 us 63 | ratio: 43.92 64 | 65 | type creation from string with 7 field(s): namedtuple('NT', 'a b c d e f g') 66 | collections: 353 us +- 5 us 67 | cnamedtuple: 8.11 us +- 0.18 us 68 | ratio: 43.46 69 | 70 | type creation from string with 8 field(s): namedtuple('NT', 'a b c d e f g h') 71 | collections: 369 us +- 7 us 72 | cnamedtuple: 8.34 us +- 0.20 us 73 | ratio: 44.18 74 | 75 | type creation from sequence with 1 field(s): namedtuple('NT', ['a']) 76 | collections: 252 us +- 4 us 77 | cnamedtuple: 6.21 us +- 0.10 us 78 | ratio: 40.56 79 | 80 | type creation from sequence with 2 field(s): namedtuple('NT', ['a', 'b']) 81 | collections: 351 us +- 44 us 82 | cnamedtuple: 6.57 us +- 0.18 us 83 | ratio: 53.42 84 | 85 | type creation from sequence with 3 field(s): namedtuple('NT', ['a', 'b', 'c']) 86 | collections: 289 us +- 6 us 87 | cnamedtuple: 6.77 us +- 0.23 us 88 | ratio: 42.66 89 | 90 | type creation from sequence with 4 field(s): namedtuple('NT', ['a', 'b', 'c', 'd']) 91 | collections: 310 us +- 12 us 92 | cnamedtuple: 6.92 us +- 0.21 us 93 | ratio: 44.84 94 | 95 | type creation from sequence with 5 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e']) 96 | collections: 322 us +- 3 us 97 | cnamedtuple: 7.31 us +- 0.16 us 98 | ratio: 44.02 99 | 100 | type creation from sequence with 6 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e', 'f']) 101 | collections: 336 us +- 5 us 102 | cnamedtuple: 7.52 us +- 0.18 us 103 | ratio: 44.72 104 | 105 | type creation from sequence with 7 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e', 'f', 'g']) 106 | collections: 352 us +- 6 us 107 | cnamedtuple: 8.03 us +- 0.19 us 108 | ratio: 43.83 109 | 110 | type creation from sequence with 8 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']) 111 | collections: 366 us +- 4 us 112 | cnamedtuple: 8.25 us +- 0.18 us 113 | ratio: 44.38 114 | 115 | type instance creation with positional arguments and 1 field(s): NT(0) 116 | collections: 333 ns +- 8 ns 117 | cnamedtuple: 221 ns +- 5 ns 118 | ratio: 1.51 119 | 120 | type instance creation with positional arguments and 2 field(s): NT(0, 1) 121 | collections: 344 ns +- 10 ns 122 | cnamedtuple: 228 ns +- 5 ns 123 | ratio: 1.51 124 | 125 | type instance creation with positional arguments and 3 field(s): NT(0, 1, 2) 126 | collections: 355 ns +- 8 ns 127 | cnamedtuple: 234 ns +- 7 ns 128 | ratio: 1.51 129 | 130 | type instance creation with positional arguments and 4 field(s): NT(0, 1, 2, 3) 131 | collections: 362 ns +- 6 ns 132 | cnamedtuple: 242 ns +- 5 ns 133 | ratio: 1.49 134 | 135 | type instance creation with positional arguments and 5 field(s): NT(0, 1, 2, 3, 4) 136 | collections: 378 ns +- 14 ns 137 | cnamedtuple: 249 ns +- 8 ns 138 | ratio: 1.52 139 | 140 | type instance creation with positional arguments and 6 field(s): NT(0, 1, 2, 3, 4, 5) 141 | collections: 382 ns +- 9 ns 142 | cnamedtuple: 254 ns +- 6 ns 143 | ratio: 1.51 144 | 145 | type instance creation with positional arguments and 7 field(s): NT(0, 1, 2, 3, 4, 5, 6) 146 | collections: 397 ns +- 10 ns 147 | cnamedtuple: 262 ns +- 11 ns 148 | ratio: 1.52 149 | 150 | type instance creation with positional arguments and 8 field(s): NT(0, 1, 2, 3, 4, 5, 6, 7) 151 | collections: 422 ns +- 11 ns 152 | cnamedtuple: 269 ns +- 5 ns 153 | ratio: 1.57 154 | 155 | type instance creation with keyword arguments and 1 field(s): NT(a=0) 156 | collections: 421 ns +- 7 ns 157 | cnamedtuple: 269 ns +- 5 ns 158 | ratio: 1.56 159 | 160 | type instance creation with keyword arguments and 2 field(s): NT(a=0, b=1) 161 | collections: 455 ns +- 7 ns 162 | cnamedtuple: 300 ns +- 6 ns 163 | ratio: 1.52 164 | 165 | type instance creation with keyword arguments and 3 field(s): NT(a=0, b=1, c=2) 166 | collections: 491 ns +- 15 ns 167 | cnamedtuple: 330 ns +- 12 ns 168 | ratio: 1.49 169 | 170 | type instance creation with keyword arguments and 4 field(s): NT(a=0, b=1, c=2, d=3) 171 | collections: 526 ns +- 11 ns 172 | cnamedtuple: 357 ns +- 6 ns 173 | ratio: 1.47 174 | 175 | type instance creation with keyword arguments and 5 field(s): NT(a=0, b=1, c=2, d=3, e=4) 176 | collections: 572 ns +- 9 ns 177 | cnamedtuple: 388 ns +- 11 ns 178 | ratio: 1.47 179 | 180 | type instance creation with keyword arguments and 6 field(s): NT(a=0, b=1, c=2, d=3, e=4, f=5) 181 | collections: 666 ns +- 20 ns 182 | cnamedtuple: 469 ns +- 15 ns 183 | ratio: 1.42 184 | 185 | type instance creation with keyword arguments and 7 field(s): NT(a=0, b=1, c=2, d=3, e=4, f=5, g=6) 186 | collections: 698 ns +- 17 ns 187 | cnamedtuple: 493 ns +- 11 ns 188 | ratio: 1.42 189 | 190 | type instance creation with keyword arguments and 8 field(s): NT(a=0, b=1, c=2, d=3, e=4, f=5, g=6, h=7) 191 | collections: 741 ns +- 15 ns 192 | cnamedtuple: 526 ns +- 17 ns 193 | ratio: 1.41 194 | 195 | field access: instance.b 196 | collections: 45.2 ns +- 1.2 ns 197 | cnamedtuple: 24.8 ns +- 0.8 ns 198 | ratio: 1.82 199 | 200 | 201 | median ratio: 1.82 202 | 203 | Contributing 204 | ------------ 205 | 206 | The project is hosted on 207 | `github `__. 208 | 209 | Before submitting a patch, please make sure your Python code is 210 | `PEP8 `__ compliant and your 211 | c code is `PEP7 `__ 212 | compliant. 213 | 214 | Contact 215 | ------- 216 | 217 | Please file all bug reports on 218 | `github `__. 219 | 220 | For questions or comments, feel free to email me at joe@quantopian.com 221 | -------------------------------------------------------------------------------- /cnamedtuple/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from cnamedtuple._namedtuple import namedtuple, _register_asdict 4 | 5 | __all__ = [ 6 | 'namedtuple' 7 | ] 8 | 9 | __version__ = '0.1.6' 10 | 11 | # Register `OrderedDict` as the constructor to use when calling `_asdict`. 12 | # This step exists because at one point there was work being done to move 13 | # this project into Python 3.5, and this works to solve a circular dependency 14 | # between 'cnamedtuple/_namedtuple.c' ('Modules/_collectionsmodule.c' 15 | # in cpython) and 'Lib/collections.py'. 16 | # 17 | # However, after discussion with the CPython folks, it was determined that 18 | # this project will not be moved in after all, and will remain as a 19 | # a third-party project. 20 | _register_asdict(OrderedDict) 21 | 22 | # Clean up the namespace for this module, the only public api should be 23 | # `namedtuple`. 24 | del _register_asdict 25 | del OrderedDict 26 | -------------------------------------------------------------------------------- /cnamedtuple/_namedtuple.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | #include "structmember.h" 3 | 4 | /* The values that the module will hold. These are needed by various functions 5 | supporting the namedtuple type. */ 6 | typedef struct{ 7 | PyObject *iskeyword; /* `keywords.iskeyword` */ 8 | PyObject *asdict; /* The constructor called from `_asdict`. */ 9 | }module_state; 10 | 11 | /* The type of the descriptors that access the named fields of the `namedtuple` 12 | types. */ 13 | typedef struct{ 14 | PyObject id_ob; 15 | Py_ssize_t id_idx; /* The index that this will access. */ 16 | }namedtuple_indexer; 17 | 18 | /* Index a tuple at `self`'s index like a specialized `itemgetter`. */ 19 | static PyObject * 20 | namedtuple_indexer_descr_get(PyObject *self, 21 | PyObject *instance, 22 | PyObject *owner) 23 | { 24 | PyObject *ret; 25 | 26 | if (!instance) { 27 | /* If this is called on the owner, then just return this object. */ 28 | ret = self; 29 | } 30 | else { 31 | /* Assert that this is a tuple subclass. */ 32 | if (!PyTuple_Check(instance)) { 33 | PyErr_Format(PyExc_TypeError, 34 | "%s objects can only be used on tuple types", 35 | ((PyTypeObject*) self->ob_type)->tp_name); 36 | return NULL; 37 | } 38 | 39 | if (!(ret = PyTuple_GetItem(instance, 40 | ((namedtuple_indexer*) self)->id_idx))) { 41 | return NULL; 42 | } 43 | } 44 | 45 | Py_INCREF(ret); 46 | return ret; 47 | } 48 | 49 | PyDoc_STRVAR(namedtuple_indexer_doc, 50 | "A specialized 'itemgetter' for tuples where the index is known to be valid."); 51 | 52 | /* A specialized `itemgetter` for tuples where the index is known to be 53 | valid. This cannot be constructed or subclassed from within python 54 | because if the preconditions are not followed, you would get undefined 55 | behaviour. */ 56 | PyTypeObject namedtuple_indexer_type = { 57 | PyVarObject_HEAD_INIT(&PyType_Type, 0) 58 | "_collections.NamedTupleIndexerType", /* tp_name */ 59 | sizeof(namedtuple_indexer), /* tp_basicsize */ 60 | 0, /* tp_itemsize */ 61 | 0, /* tp_dealloc */ 62 | 0, /* tp_print */ 63 | 0, /* tp_getattr */ 64 | 0, /* tp_setattr */ 65 | 0, /* tp_reserved */ 66 | 0, /* tp_repr */ 67 | 0, /* tp_as_number */ 68 | 0, /* tp_as_sequence */ 69 | 0, /* tp_as_mapping */ 70 | 0, /* tp_hash */ 71 | 0, /* tp_call */ 72 | 0, /* tp_str */ 73 | PyObject_GenericGetAttr, /* tp_getattro */ 74 | 0, /* tp_setattro */ 75 | 0, /* tp_as_buffer */ 76 | Py_TPFLAGS_DEFAULT, /* tp_flags */ 77 | namedtuple_indexer_doc, /* tp_doc */ 78 | 0, /* tp_traverse */ 79 | 0, /* tp_clear */ 80 | 0, /* tp_richcompare */ 81 | 0, /* tp_weaklistoffset */ 82 | 0, /* tp_iter */ 83 | 0, /* tp_iternext */ 84 | 0, /* tp_methods */ 85 | 0, /* tp_members */ 86 | 0, /* tp_getset */ 87 | 0, /* tp_base */ 88 | 0, /* tp_dict */ 89 | namedtuple_indexer_descr_get, /* tp_descr_get */ 90 | 0, /* tp_descr_set */ 91 | 0, /* tp_dictoffset */ 92 | 0, /* tp_init */ 93 | 0, /* tp_alloc */ 94 | 0, /* tp_new */ 95 | }; 96 | 97 | /* A wrapper around an object to make read-only access to it. */ 98 | typedef struct{ 99 | PyObject wr_ob; 100 | PyObject *wr_wrapped; 101 | }namedtuple_descr_wrapper; 102 | 103 | static void 104 | namedtuple_descr_wrapper_dealloc(PyObject *self) 105 | { 106 | Py_CLEAR(((namedtuple_descr_wrapper*) self)->wr_wrapped); 107 | PyObject_Del(self); 108 | } 109 | 110 | /* Retrieve the wrapped object. Because this will return the object even 111 | when `instance` is NULL, this is kind of like a class property. */ 112 | static PyObject * 113 | namedtuple_descr_wrapper_get(PyObject *self, 114 | PyObject *instance, 115 | PyObject *owner) 116 | { 117 | PyObject *ret = ((namedtuple_descr_wrapper*) self)->wr_wrapped; 118 | Py_INCREF(ret); 119 | return ret; 120 | } 121 | 122 | PyDoc_STRVAR(namedtuple_descr_wrapper_doc, 123 | "A wrapper for objects for using the descriptor protocol."); 124 | 125 | PyTypeObject namedtuple_descr_wrapper_type = { 126 | PyVarObject_HEAD_INIT(&PyType_Type, 0) 127 | "_collections.NamedTupleDescrWrapper", /* tp_name */ 128 | sizeof(namedtuple_descr_wrapper), /* tp_basicsize */ 129 | 0, /* tp_itemsize */ 130 | (destructor) namedtuple_descr_wrapper_dealloc, /* tp_dealloc */ 131 | 0, /* tp_print */ 132 | 0, /* tp_getattr */ 133 | 0, /* tp_setattr */ 134 | 0, /* tp_reserved */ 135 | 0, /* tp_repr */ 136 | 0, /* tp_as_number */ 137 | 0, /* tp_as_sequence */ 138 | 0, /* tp_as_mapping */ 139 | 0, /* tp_hash */ 140 | 0, /* tp_call */ 141 | 0, /* tp_str */ 142 | PyObject_GenericGetAttr, /* tp_getattro */ 143 | 0, /* tp_setattro */ 144 | 0, /* tp_as_buffer */ 145 | Py_TPFLAGS_DEFAULT, /* tp_flags */ 146 | namedtuple_descr_wrapper_doc, /* tp_doc */ 147 | 0, /* tp_traverse */ 148 | 0, /* tp_clear */ 149 | 0, /* tp_richcompare */ 150 | 0, /* tp_weaklistoffset */ 151 | 0, /* tp_iter */ 152 | 0, /* tp_iternext */ 153 | 0, /* tp_methods */ 154 | 0, /* tp_members */ 155 | 0, /* tp_getset */ 156 | 0, /* tp_base */ 157 | 0, /* tp_dict */ 158 | namedtuple_descr_wrapper_get, /* tp_descr_get */ 159 | 0, /* tp_descr_set */ 160 | 0, /* tp_dictoffset */ 161 | 0, /* tp_init */ 162 | 0, /* tp_alloc */ 163 | 0, /* tp_new */ 164 | }; 165 | 166 | /* Gets the `_fields` off a namedtuple. Raises a `TypeError` error if this is 167 | not a tuple. 168 | return: A new reference or NULL */ 169 | static PyObject * 170 | get_fields(PyObject *self) { 171 | PyObject *fields; 172 | 173 | if (!(fields = PyObject_GetAttrString(self, "_fields"))) { 174 | return NULL; 175 | } 176 | 177 | if (!(PyTuple_CheckExact(fields))) { 178 | PyErr_SetString(PyExc_TypeError, 179 | "_fields must be a tuple of fieldnames."); 180 | Py_DECREF(fields); 181 | return NULL; 182 | } 183 | 184 | return fields; 185 | } 186 | 187 | /* `__new__` for namedtuple types. This will reflect it's argument list 188 | off `cls`. 189 | return: A new instance of a namedtuple or NULL in case of error. */ 190 | static PyObject * 191 | namedtuple_new(PyTypeObject *cls, PyObject *args, PyObject *kwargs) 192 | { 193 | PyObject *self; 194 | PyObject *fields; 195 | Py_ssize_t fieldc; 196 | PyObject *keyword; 197 | Py_ssize_t n; 198 | Py_ssize_t pos; 199 | Py_ssize_t nargs; 200 | Py_ssize_t nkwargs; 201 | PyObject *current_arg; 202 | int match; 203 | PyObject *key; 204 | PyObject *value; 205 | 206 | if (!(fields = get_fields((PyObject*) cls))) { 207 | return NULL; 208 | } 209 | fieldc = PyTuple_GET_SIZE(fields); 210 | 211 | nargs = PyTuple_GET_SIZE(args); 212 | nkwargs = (kwargs) ? PyDict_Size(kwargs) : 0; 213 | 214 | if (!(self = cls->tp_alloc(cls, fieldc))) { 215 | Py_DECREF(fields); 216 | return NULL; 217 | } 218 | /* zero the tuple so we can decref at any time */ 219 | memset(((PyTupleObject*) self)->ob_item, 0, sizeof(PyObject*) * fieldc); 220 | 221 | // Custom argument parsing because of dynamic construction. 222 | if (nargs + nkwargs > fieldc) { 223 | PyErr_Format(PyExc_TypeError, 224 | "%U takes at most %zd argument%s (%zd given)", 225 | ((PyHeapTypeObject*) cls)->ht_name, 226 | fieldc, 227 | (fieldc == 1) ? "" : "s", 228 | nargs + nkwargs); 229 | 230 | goto error; 231 | } 232 | 233 | for (n = 0;n < fieldc;n++) { 234 | keyword = PyTuple_GET_ITEM(fields, n); 235 | current_arg = NULL; 236 | if (nkwargs) { 237 | current_arg = PyDict_GetItem(kwargs, keyword); 238 | } 239 | if (current_arg) { 240 | --nkwargs; 241 | if (n < nargs) { 242 | /* Arg present in tuple and in dict. */ 243 | PyErr_Format(PyExc_TypeError, 244 | "Argument given by name ('%U') and position (%zd)", 245 | keyword, 246 | n + 1); 247 | goto error; 248 | } 249 | } 250 | else if (nkwargs && PyErr_Occurred()) { 251 | goto error; 252 | } 253 | else if (n < nargs) { 254 | current_arg = PyTuple_GET_ITEM(args,n); 255 | } 256 | 257 | if (current_arg) { 258 | /* This reference is stolen when we store it in self. */ 259 | Py_INCREF(current_arg); 260 | PyTuple_SET_ITEM((PyTupleObject*) self, n, current_arg); 261 | continue; 262 | } 263 | 264 | if (n < fieldc) { 265 | PyErr_Format(PyExc_TypeError, 266 | "Required argument '%U' (pos %zd) not found", 267 | keyword, 268 | n + 1); 269 | goto error; 270 | } 271 | } 272 | 273 | /* Check for extra kwargs. */ 274 | if (nkwargs > 0) { 275 | pos = 0; 276 | while (PyDict_Next(kwargs, &pos, &key, &value)) { 277 | if (!PyUnicode_Check(key)) { 278 | PyErr_SetString(PyExc_TypeError, 279 | "keywords must be strings"); 280 | goto error; 281 | } 282 | for (n = 0;n < fieldc;++n) { 283 | if (!PyUnicode_Compare(key, PyTuple_GET_ITEM(fields, n))) { 284 | match = 1; 285 | break; 286 | } 287 | } 288 | if (!match) { 289 | PyErr_Format(PyExc_TypeError, 290 | "'%U' is an invalid keyword argument for this " 291 | "function", 292 | key); 293 | goto error; 294 | } 295 | } 296 | } 297 | 298 | Py_DECREF(fields); 299 | return self; 300 | error: 301 | 302 | Py_DECREF(fields); 303 | Py_DECREF(self); 304 | return NULL; 305 | } 306 | 307 | /* Namedtuple class method for creating new instances from an iterable. 308 | return `PyObject*` representing the new instance, or NULL to signal an 309 | error. */ 310 | static PyObject * 311 | namedtuple__make(PyObject *cls, PyObject *args, PyObject *kwargs) 312 | { 313 | const char * const argnames[] = {"iterable", NULL}; 314 | PyObject *iterable; 315 | PyObject *ret; 316 | 317 | if (!PyArg_ParseTupleAndKeywords(args, 318 | kwargs, 319 | "O:_make", 320 | (char**) argnames, 321 | &iterable)) { 322 | return NULL; 323 | } 324 | 325 | if (!(iterable = PySequence_Tuple(iterable))) { 326 | PyErr_SetString(PyExc_ValueError, "iterable must be a sequence"); 327 | return NULL; 328 | } 329 | 330 | ret = ((PyTypeObject*) cls)->tp_new((PyTypeObject*) cls, iterable, NULL); 331 | Py_DECREF(iterable); 332 | return ret; 333 | } 334 | 335 | /* return: new instance of `type(self)` with the kwargs 336 | swapped out. On failure, returns NULL. */ 337 | static PyObject * 338 | namedtuple__replace(PyObject *self, PyObject *args, PyObject *kwargs) 339 | { 340 | PyObject *arg; 341 | PyObject *items; 342 | PyObject *item; 343 | PyObject *ret; 344 | PyObject *tkeys; 345 | Py_ssize_t n; 346 | PyObject *fields; 347 | Py_ssize_t fieldc; 348 | 349 | if (!(fields = get_fields(self))) { 350 | return NULL; 351 | } 352 | fieldc = PyTuple_GET_SIZE(fields); 353 | 354 | if (PyTuple_GET_SIZE(args)) { 355 | /* No positional arguments allowed. */ 356 | PyErr_Format(PyExc_TypeError, 357 | "_replace takes no positional arguments (%zd given)", 358 | PyTuple_GET_SIZE(args)); 359 | Py_DECREF(fields); 360 | return NULL; 361 | } 362 | 363 | if (!kwargs) { 364 | /* Fast path if nothing needs to be replaced, just copy. */ 365 | if (!(arg = PyTuple_Pack(1, self))) { 366 | Py_DECREF(fields); 367 | return NULL; 368 | } 369 | ret = namedtuple__make((PyObject*) self->ob_type, arg, NULL); 370 | Py_DECREF(arg); 371 | return ret; 372 | } 373 | 374 | if (!(items = PyTuple_New(fieldc))) { 375 | Py_DECREF(fields); 376 | return NULL; 377 | } 378 | 379 | for (n = 0;n < fieldc;++n) { 380 | if (!(item = PyDict_GetItem(kwargs, PyTuple_GET_ITEM(fields, n)))) { 381 | item = PyTuple_GET_ITEM(self, n); 382 | } 383 | else { 384 | PyDict_DelItem(kwargs, PyTuple_GET_ITEM(fields, n)); 385 | } 386 | 387 | Py_INCREF(item); 388 | PyTuple_SET_ITEM(items, n, item); 389 | } 390 | 391 | if (PyDict_Size(kwargs)) { 392 | tkeys = PyDict_Keys(kwargs); 393 | PyErr_Format(PyExc_ValueError, "Got unexpected field names: %R", tkeys); 394 | Py_DECREF(tkeys); 395 | Py_DECREF(fields); 396 | Py_DECREF(items); 397 | return NULL; 398 | } 399 | 400 | Py_DECREF(fields); 401 | 402 | arg = PyTuple_Pack(1, items); 403 | Py_DECREF(items); 404 | if (!arg) { 405 | return NULL; 406 | } 407 | ret = namedtuple__make((PyObject*) self->ob_type, arg, NULL); 408 | Py_DECREF(arg); 409 | return ret; 410 | } 411 | 412 | /* The `__repr__` for `namedtuple` objects. 413 | return: A str in the format `{typename}({f_1}={v_1}, ..., {f_n}={v_n})` 414 | or NULL in case of an exception. */ 415 | static PyObject * 416 | namedtuple_repr(PyObject *self, PyObject *_) 417 | { 418 | PyObject *reprfmt; 419 | Py_ssize_t len; 420 | Py_ssize_t n; 421 | PyObject *field; 422 | PyObject *args; 423 | PyObject *ret; 424 | 425 | if (!(reprfmt = PyObject_GetAttrString(self, "__reprfmt__"))) { 426 | return NULL; 427 | } 428 | 429 | if (!PyUnicode_CheckExact(reprfmt)) { 430 | PyErr_SetString(PyExc_TypeError, 431 | "__reprfmt__ must be an instance of 'str'"); 432 | Py_DECREF(reprfmt); 433 | return NULL; 434 | } 435 | 436 | len = PyTuple_GET_SIZE(self); 437 | if (!(args = PyTuple_New(len + 1))) { 438 | Py_DECREF(reprfmt); 439 | return NULL; 440 | } 441 | 442 | PyTuple_SET_ITEM(args, 0, ((PyHeapTypeObject*) self->ob_type)->ht_name); 443 | Py_INCREF(((PyHeapTypeObject*) self->ob_type)->ht_name); 444 | 445 | for (n = 1;n < len + 1;++n) { 446 | field = PyTuple_GET_ITEM(self, n - 1); 447 | Py_INCREF(field); 448 | PyTuple_SET_ITEM(args, n, field); 449 | } 450 | 451 | ret = PyUnicode_Format(reprfmt, args); 452 | Py_DECREF(reprfmt); 453 | Py_DECREF(args); 454 | return ret; 455 | } 456 | 457 | /* Converts self into a dict. 458 | return: A new dict or NULL in case of error. */ 459 | PyObject * 460 | namedtuple__asdict(PyObject *self, PyObject *_) 461 | { 462 | Py_ssize_t n; 463 | PyObject *fields; 464 | Py_ssize_t fieldc; 465 | PyObject *args; 466 | PyObject *ret; 467 | PyObject *asdict; 468 | 469 | if (!(fields = get_fields(self))) { 470 | return NULL; 471 | } 472 | fieldc = PyTuple_GET_SIZE(fields); 473 | 474 | if (!(args = PyTuple_New(fieldc))) { 475 | Py_DECREF(fields); 476 | return NULL; 477 | } 478 | for (n = 0;n < fieldc;++n) { 479 | if (!(ret = PyTuple_Pack(2, 480 | PyTuple_GET_ITEM(fields, n), 481 | PyTuple_GET_ITEM(self, n)))) { 482 | Py_DECREF(args); 483 | Py_DECREF(fields); 484 | return NULL; 485 | } 486 | PyTuple_SET_ITEM(args, n, ret); 487 | } 488 | Py_DECREF(fields); 489 | 490 | if (!(asdict = PyObject_GetAttrString(self, "__asdict__"))) { 491 | Py_DECREF(args); 492 | return NULL; 493 | } 494 | 495 | ret = PyObject_CallFunctionObjArgs(asdict, args, NULL); 496 | Py_DECREF(asdict); 497 | Py_DECREF(args); 498 | return ret; 499 | } 500 | 501 | /* Pickle and copy protocol. 502 | return: self as a plain tuple or NULL in case of error. */ 503 | static PyObject * 504 | namedtuple_getnewargs(PyObject *self, PyObject *_) 505 | { 506 | return PySequence_Tuple(self); 507 | } 508 | 509 | /* Pass function for the pickle and copy protocol. 510 | return: Py_None. */ 511 | static PyObject * 512 | namedtuple_getstate(PyObject *self, PyObject *_) 513 | { 514 | Py_RETURN_NONE; 515 | } 516 | 517 | /* Pickle protocol for extension types. 518 | return: A tuple `(type(self),tuple(self))` or NULL in case of an error. */ 519 | static PyObject * 520 | namedtuple_reduce_ex(PyObject *self,PyObject *_) 521 | { 522 | PyObject *astuple = PySequence_Tuple(self); 523 | PyObject *ret; 524 | 525 | if (!astuple) { 526 | return NULL; 527 | } 528 | 529 | ret = PyTuple_Pack(2, self->ob_type, astuple); 530 | Py_DECREF(astuple); 531 | return ret; 532 | } 533 | 534 | static int 535 | namedtuple_traverse(PyTupleObject *self, visitproc visit, void *arg) 536 | { 537 | Py_ssize_t n; 538 | 539 | for (n = PyTuple_GET_SIZE(self);--n >= 0;) { 540 | Py_VISIT(PyTuple_GET_ITEM(self, n)); 541 | } 542 | return 0; 543 | } 544 | 545 | /* The `__dict__` of namedtuple types is the `__asdict__` type holding all 546 | of the fields. 547 | return: A new dict or NULL in case of error. */ 548 | static PyObject * 549 | namedtuple_get_dict(PyObject *self, void *_) 550 | { 551 | PyObject *asdict = PyObject_GetAttrString(self, "__asdict__"); 552 | Py_ssize_t n; 553 | PyObject *fields; 554 | Py_ssize_t fieldc; 555 | PyObject *arg; 556 | PyObject *tmp; 557 | 558 | if (!asdict) { 559 | return NULL; 560 | } 561 | 562 | if (!(fields = get_fields(self))) { 563 | return NULL; 564 | } 565 | 566 | fieldc = PyTuple_GET_SIZE(fields); 567 | if (!(arg = PyTuple_New(fieldc))) { 568 | Py_DECREF(fields); 569 | return NULL; 570 | } 571 | 572 | /* Construct an assoc list of ((field . value)) to pass to 573 | the `__asdict__` constructor. */ 574 | for (n = 0;n < fieldc;++n) { 575 | if (!(tmp = PyTuple_Pack(2, 576 | PyTuple_GET_ITEM(fields, n), 577 | PyTuple_GET_ITEM(self, n)))) { 578 | Py_DECREF(arg); 579 | Py_DECREF(fields); 580 | return NULL; 581 | } 582 | 583 | PyTuple_SET_ITEM(arg, n, tmp); 584 | } 585 | 586 | tmp = PyObject_CallFunctionObjArgs(asdict, arg, NULL); 587 | Py_DECREF(arg); 588 | Py_DECREF(fields); 589 | return tmp; 590 | } 591 | 592 | PyDoc_STRVAR(_make_doc, 593 | "_make(iterable) -> namedtuple\n\n" 594 | "Create an instance of this class from an iterable."); 595 | 596 | PyDoc_STRVAR(_replace_doc, 597 | "_replace(field=new_value, ...) -> new namedtuple\n\n" 598 | "Returns a new namedtuple with the specified fields replaced."); 599 | 600 | PyDoc_STRVAR(_asdict_doc, 601 | "_asdict() -> dict\n\n" 602 | "Converts this namedtuple into a dictionary that maps fields to values"); 603 | 604 | PyDoc_STRVAR(__getnewargs___doc, 605 | "__getnewargs__() -> tuple\n\n" 606 | "Return self as a plain tuple. Used by copy and pickle."); 607 | 608 | PyDoc_STRVAR(__getstate___doc, 609 | "__getstate__() -> None\n\n" 610 | "Exclude the OrderedDict from pickling."); 611 | 612 | PyDoc_STRVAR(__reduce_ex___doc, 613 | "__reduce_ex__() -> (type(self), tuple(self))\n\n" 614 | "Returns the pair of the type of the instance with the instance cast to\n" 615 | "a tuple."); 616 | 617 | PyMethodDef namedtuple_methods[] = { 618 | {"_make", 619 | (PyCFunction) namedtuple__make, 620 | METH_CLASS | METH_VARARGS | METH_KEYWORDS, 621 | _make_doc}, 622 | {"_replace", 623 | (PyCFunction) namedtuple__replace, 624 | METH_VARARGS | METH_KEYWORDS, 625 | _replace_doc}, 626 | {"_asdict", 627 | (PyCFunction) namedtuple__asdict, 628 | METH_NOARGS, 629 | _asdict_doc}, 630 | {"__getnewargs__", 631 | (PyCFunction) namedtuple_getnewargs, 632 | METH_NOARGS, 633 | __getnewargs___doc}, 634 | {"__getstate__", 635 | namedtuple_getstate, 636 | METH_NOARGS, 637 | __getstate___doc}, 638 | {"__reduce_ex__", 639 | namedtuple_reduce_ex, 640 | METH_O, 641 | __reduce_ex___doc}, 642 | {NULL}, 643 | }; 644 | 645 | /* Construct the `fields` tuple from the input `field_names`. */ 646 | static PyObject * 647 | build_fields(PyObject *field_names) 648 | { 649 | PyObject *tmp_fields; 650 | PyObject *fast_fields; 651 | PyObject *fields; 652 | PyObject *with_replace; 653 | PyObject *as_str; 654 | PyObject *comma; 655 | PyObject *space; 656 | Py_ssize_t n; 657 | Py_ssize_t len; 658 | 659 | /* If the `field_names` is a `str`, then we will replace all ',' with ' ' 660 | and then split it on whitespace to get the sequence of fields. */ 661 | if (PyUnicode_Check(field_names)) { 662 | if (!(comma = PyUnicode_InternFromString(","))) { 663 | return NULL; 664 | } 665 | if (!(space = PyUnicode_InternFromString(" "))) { 666 | Py_DECREF(comma); 667 | return NULL; 668 | } 669 | 670 | /* Replace all instances of ',' with ' '. */ 671 | with_replace = PyUnicode_Replace(field_names, comma, space, -1); 672 | Py_DECREF(comma); 673 | Py_DECREF(space); 674 | 675 | if (!with_replace) { 676 | return NULL; 677 | } 678 | 679 | // Split the field names into a tuple around all whitespace. 680 | tmp_fields = PyUnicode_Split(with_replace, NULL, -1); 681 | Py_DECREF(with_replace); 682 | 683 | if (!tmp_fields) { 684 | return NULL; 685 | } 686 | } 687 | else { 688 | /* The `field_names` is a sequence already, just convert it into a 689 | tuple. */ 690 | if (!(tmp_fields = PySequence_Tuple(field_names))) { 691 | return NULL; 692 | } 693 | } 694 | 695 | fast_fields = PySequence_Fast(tmp_fields, "field_names must be a sequence"); 696 | Py_DECREF(tmp_fields); 697 | if (!fast_fields) { 698 | return NULL; 699 | } 700 | len = PySequence_Fast_GET_SIZE(fast_fields); 701 | fields = PyTuple_New(len); 702 | 703 | for (n = 0;n < len;++n) { 704 | if (!(as_str = 705 | PyObject_Str(PySequence_Fast_GET_ITEM(fast_fields, n)))) { 706 | Py_DECREF(tmp_fields); 707 | Py_DECREF(fields); 708 | return NULL; 709 | } 710 | PyTuple_SET_ITEM(fields, n, as_str); 711 | } 712 | 713 | Py_DECREF(tmp_fields); 714 | return fields; 715 | } 716 | 717 | /* A type to indicate the results of checking a field or type name */ 718 | typedef enum{ 719 | CHECKFIELD_VALID = 0, 720 | CHECKFIELD_EMPTY = 1, 721 | CHECKFIELD_DIGIT = 2, 722 | CHECKFIELD_UNDERSCORE = 3, 723 | CHECKFIELD_NONALNUM = 4, 724 | CHECKFIELD_KEYWORD = 5, 725 | CHECKFIELD_NOTREADY = 6, 726 | }nt_checkfield; 727 | 728 | /* Checks the field against the various rules for invalid names. 729 | return: An `nt_checkfield` indicating the result. */ 730 | static nt_checkfield 731 | checkfield(PyObject *iskeyword, PyObject *field) 732 | { 733 | void *data; 734 | int kind; 735 | Py_UCS4 ch; 736 | Py_ssize_t idx; 737 | PyObject *iskwd_obj; 738 | int iskwd; 739 | 740 | if (PyUnicode_READY(field)) { 741 | return CHECKFIELD_NOTREADY; 742 | } 743 | 744 | if (!PyUnicode_GET_LENGTH(field)) { 745 | /* Empty name. */ 746 | return CHECKFIELD_EMPTY; 747 | } 748 | 749 | kind = PyUnicode_KIND(field); 750 | data = PyUnicode_DATA(field); 751 | ch = PyUnicode_READ(kind, data, 0); 752 | 753 | if (Py_UNICODE_ISDIGIT(ch)) { 754 | /* Cannot start with digit. */ 755 | return CHECKFIELD_DIGIT; 756 | } 757 | else if (ch == (Py_UCS4) '_') { 758 | /* Cannot start with '_'. */ 759 | return CHECKFIELD_UNDERSCORE; 760 | } 761 | 762 | 763 | idx = PyUnicode_GET_LENGTH(field); 764 | while (idx--) { 765 | ch = PyUnicode_READ(kind, data, idx); 766 | if (!(Py_UNICODE_ISALNUM(ch) || ch == (Py_UCS4) '_')) { 767 | /* Must be all alphanumeric characters or underscores. */ 768 | return CHECKFIELD_NONALNUM; 769 | } 770 | } 771 | 772 | iskwd = ((iskwd_obj = PyObject_CallFunctionObjArgs(iskeyword, 773 | field, 774 | NULL)) && 775 | PyObject_IsTrue(iskwd_obj)); 776 | Py_XDECREF(iskwd_obj); 777 | 778 | if (iskwd) { 779 | return CHECKFIELD_KEYWORD; 780 | } 781 | 782 | return CHECKFIELD_VALID; 783 | } 784 | 785 | /* Rename the `field_names` if they fail the various checks. This mutates 786 | `fields` in place. 787 | return: Zero on succes, nonzero on failure */ 788 | static int 789 | rename_fields(PyObject *iskeyword, PyObject *fields) 790 | { 791 | PyObject *seen; 792 | PyObject *field; 793 | Py_ssize_t fieldc = PyTuple_GET_SIZE(fields); 794 | Py_ssize_t n; 795 | int rename; 796 | 797 | if (!(seen = PySet_New(NULL))) { 798 | return -1; 799 | } 800 | 801 | /* Iterate over the fields, applying any renames if needed. */ 802 | for (n = 0;n < fieldc;++n) { 803 | rename = 0; 804 | field = PyTuple_GET_ITEM(fields, n); 805 | 806 | if (checkfield(iskeyword, field) 807 | != CHECKFIELD_VALID) { 808 | // Invalid name for some reason. 809 | rename = 1; 810 | } 811 | else { 812 | /* Check if the name is in the set of seen names. */ 813 | switch(PySet_Contains(seen, field)) { 814 | case 1: 815 | rename = 1; 816 | break; 817 | case -1: 818 | Py_DECREF(seen); 819 | return -1; 820 | } 821 | } 822 | 823 | if (rename) { 824 | Py_DECREF(field); 825 | PyTuple_SET_ITEM(fields, n, NULL); 826 | 827 | if (!(field = PyUnicode_FromFormat("_%zu", n))) { 828 | Py_DECREF(seen); 829 | return -1; 830 | } 831 | PyTuple_SET_ITEM(fields, n, field); 832 | } 833 | 834 | /* Add the name to the set of seen names. */ 835 | if (PySet_Add(seen, field)) { 836 | Py_DECREF(seen); 837 | return -1; 838 | } 839 | } 840 | 841 | Py_DECREF(seen); 842 | return 0; 843 | } 844 | 845 | /* Process the `typename` and `field_names` for a `namedtuple`. 846 | After this function is called (and no errors occur), 847 | `field_names` will point to a new reference to a tuple of field names 848 | that have been properly renamed and checked. 849 | return: Zero on succes, nonzero on failure. */ 850 | static int 851 | validate_field_names(PyObject *typename, 852 | PyObject **field_names, 853 | int rename, 854 | PyObject *iskeyword) 855 | { 856 | const char * const nonalnum_fmt = 857 | "Type names and field names can only contain alphanumeric characters " 858 | "and underscores: %U"; 859 | const char * const keyword_fmt = 860 | "Type names and field names cannot be a keyword: %U"; 861 | const char * const digit_fmt = "Type names and field names cannot start " 862 | "with a number: %U"; 863 | const char * const underscore_fmt = 864 | "Field names cannot start with an underscore: %U"; 865 | const char * const empty_type_fmt = "Type names cannot be empty"; 866 | const char * const empty_field_fmt = 867 | "Field names cannot be empty (index %zd)"; 868 | const char * const seen_fmt = "Encountered duplicate field name: %U"; 869 | const char * const notready_fmt = "name was not ready"; 870 | PyObject *fields; 871 | PyObject *field; 872 | Py_ssize_t fieldc; 873 | Py_ssize_t n; 874 | PyObject *seen; 875 | 876 | if (!(fields = build_fields(*field_names))) { 877 | return -1; 878 | } 879 | 880 | if (rename) { 881 | if (rename_fields(iskeyword, fields)) { 882 | Py_DECREF(fields); 883 | return -1; 884 | } 885 | } 886 | 887 | switch(checkfield(iskeyword, typename)) { 888 | case CHECKFIELD_UNDERSCORE: /* typenames may begin with '_'. */ 889 | case CHECKFIELD_VALID: 890 | break; 891 | case CHECKFIELD_NONALNUM: 892 | PyErr_Format(PyExc_ValueError,nonalnum_fmt,typename); 893 | Py_DECREF(fields); 894 | return -1; 895 | case CHECKFIELD_KEYWORD: 896 | PyErr_Format(PyExc_ValueError,keyword_fmt,typename); 897 | Py_DECREF(fields); 898 | return -1; 899 | case CHECKFIELD_DIGIT: 900 | PyErr_Format(PyExc_ValueError,digit_fmt,typename); 901 | Py_DECREF(fields); 902 | return -1; 903 | case CHECKFIELD_EMPTY: 904 | PyErr_SetString(PyExc_ValueError,empty_type_fmt); 905 | Py_DECREF(fields); 906 | return -1; 907 | case CHECKFIELD_NOTREADY: 908 | PyErr_SetString(PyExc_ValueError,notready_fmt); 909 | Py_DECREF(fields); 910 | return -1; 911 | } 912 | 913 | if (!(seen = PySet_New(NULL))) { 914 | Py_DECREF(fields); 915 | return 1; 916 | } 917 | 918 | fieldc = PyTuple_GET_SIZE(fields); 919 | for (n = 0;n < fieldc;++n) { 920 | field = PyTuple_GET_ITEM(fields, n); 921 | 922 | switch(checkfield(iskeyword, field)) { 923 | case CHECKFIELD_VALID: 924 | break; 925 | case CHECKFIELD_UNDERSCORE: 926 | if (!rename) { 927 | PyErr_Format(PyExc_ValueError, underscore_fmt, field); 928 | Py_DECREF(fields); 929 | Py_DECREF(seen); 930 | return -1; 931 | } 932 | break; 933 | case CHECKFIELD_NONALNUM: 934 | PyErr_Format(PyExc_ValueError, nonalnum_fmt, field); 935 | Py_DECREF(fields); 936 | Py_DECREF(seen); 937 | return -1; 938 | case CHECKFIELD_KEYWORD: 939 | PyErr_Format(PyExc_ValueError, keyword_fmt, field); 940 | Py_DECREF(fields); 941 | Py_DECREF(seen); 942 | return -1; 943 | case CHECKFIELD_DIGIT: 944 | PyErr_Format(PyExc_ValueError, digit_fmt, field); 945 | Py_DECREF(fields); 946 | Py_DECREF(seen); 947 | return -1; 948 | case CHECKFIELD_EMPTY: 949 | PyErr_Format(PyExc_ValueError, empty_field_fmt, n); 950 | Py_DECREF(fields); 951 | Py_DECREF(seen); 952 | return -1; 953 | case CHECKFIELD_NOTREADY: 954 | PyErr_SetString(PyExc_ValueError, notready_fmt); 955 | Py_DECREF(fields); 956 | Py_DECREF(seen); 957 | return -1; 958 | } 959 | 960 | switch(PySet_Contains(seen, field)) { 961 | case 1: 962 | PyErr_Format(PyExc_ValueError, seen_fmt, field); 963 | /* fallthrough */ 964 | case -1: 965 | Py_DECREF(fields); 966 | Py_DECREF(seen); 967 | return -1; 968 | } 969 | 970 | if (PySet_Add(seen, field)) { 971 | Py_DECREF(fields); 972 | Py_DECREF(seen); 973 | return -1; 974 | } 975 | } 976 | 977 | Py_DECREF(seen); 978 | *field_names = fields; 979 | return 0; 980 | } 981 | 982 | /* Adds a `namedtuple_indexer` for each field in `fields`. 983 | return: Zero on succes, nonzero on failure. */ 984 | static int 985 | add_indexers(PyObject *dict_, PyObject *fields) 986 | { 987 | Py_ssize_t fieldc = PyTuple_GET_SIZE(fields); 988 | Py_ssize_t n; 989 | namedtuple_indexer *indexer; 990 | 991 | for (n = 0;n < fieldc;++n) { 992 | if (!(indexer = PyObject_New(namedtuple_indexer, 993 | &namedtuple_indexer_type))) { 994 | return -1; 995 | } 996 | 997 | indexer->id_idx = n; 998 | if (PyDict_SetItem(dict_, 999 | PyTuple_GET_ITEM(fields, n), 1000 | (PyObject*) indexer)) { 1001 | return -1; 1002 | } 1003 | Py_DECREF(indexer); 1004 | } 1005 | 1006 | return 0; 1007 | } 1008 | 1009 | /* Cache the repr format string so we don't need to do this every time we call 1010 | `namedtuple_repr`. 1011 | return: Zero on succes, nonzero on failure. */ 1012 | static int 1013 | cache_repr_fmt(PyObject *dict_, PyObject *fields) 1014 | { 1015 | Py_ssize_t fieldc = PyTuple_GET_SIZE(fields); 1016 | PyObject *field_fmts; 1017 | PyObject *sep; 1018 | PyObject *joined; 1019 | Py_ssize_t n; 1020 | PyObject *field_fmt; 1021 | PyObject *reprfmt = NULL; 1022 | int ret; 1023 | 1024 | /* Allocate a tuple strings representing each field's format, */ 1025 | if (!(field_fmts = PyTuple_New(fieldc))) { 1026 | return -1; 1027 | } 1028 | 1029 | for (n = 0;n < fieldc;++n) { 1030 | if (!(field_fmt = PyUnicode_FromFormat("%U=%%r", 1031 | PyTuple_GET_ITEM(fields, n)))) { 1032 | Py_DECREF(field_fmts); 1033 | return -1; 1034 | } 1035 | PyTuple_SET_ITEM(field_fmts, n, field_fmt); 1036 | } 1037 | 1038 | if (!(sep = PyUnicode_FromString(", "))) { 1039 | Py_DECREF(field_fmts); 1040 | return -1; 1041 | } 1042 | joined = PyUnicode_Join(sep, field_fmts); 1043 | Py_DECREF(sep); 1044 | Py_DECREF(field_fmts); 1045 | if (!joined) { 1046 | return -1; 1047 | } 1048 | reprfmt = PyUnicode_FromFormat("%%s(%U)", joined); 1049 | Py_DECREF(joined); 1050 | if (!reprfmt) { 1051 | return -1; 1052 | } 1053 | 1054 | ret = PyDict_SetItemString(dict_, "__reprfmt__", reprfmt); 1055 | Py_DECREF(reprfmt); 1056 | return ret; 1057 | } 1058 | 1059 | PyGetSetDef namedtuple_getsets[] = { 1060 | {"__dict__", 1061 | namedtuple_get_dict, 1062 | NULL, 1063 | NULL}, 1064 | {NULL}, 1065 | }; 1066 | 1067 | PyType_Slot namedtuple_slots[] = { 1068 | {Py_tp_new, 1069 | namedtuple_new}, 1070 | {Py_tp_methods, 1071 | namedtuple_methods}, 1072 | {Py_tp_repr, 1073 | namedtuple_repr}, 1074 | {Py_tp_traverse, 1075 | namedtuple_traverse}, 1076 | {Py_tp_getset, 1077 | namedtuple_getsets}, 1078 | {Py_tp_base, 1079 | &PyTuple_Type}, 1080 | {0, NULL}, 1081 | }; 1082 | 1083 | PyType_Spec namedtuple_spec = { 1084 | "", /* placeholder */ 1085 | 0, 1086 | 0, 1087 | Py_TPFLAGS_HAVE_GC 1088 | | Py_TPFLAGS_TUPLE_SUBCLASS 1089 | | Py_TPFLAGS_HEAPTYPE 1090 | | Py_TPFLAGS_BASETYPE 1091 | | Py_TPFLAGS_DEFAULT, 1092 | namedtuple_slots, 1093 | }; 1094 | 1095 | /* namedtuple factory function. 1096 | return: A new reference to a new namedtuple type or NULL on failure. */ 1097 | static PyObject * 1098 | namedtuple_factory(PyObject *self,PyObject *args,PyObject *kwargs) 1099 | { 1100 | const char *const argnames[] = { 1101 | "typename", 1102 | "field_names", 1103 | "rename", 1104 | NULL, 1105 | }; 1106 | module_state *st = PyModule_GetState(self); 1107 | PyObject *typename = NULL; 1108 | PyObject *field_names = NULL; 1109 | int rename = 0; 1110 | PyTypeObject *newtype; 1111 | PyObject *globals; 1112 | PyObject *module_name; 1113 | PyObject *qualname; 1114 | namedtuple_descr_wrapper *descr; 1115 | PyObject *dict_; 1116 | int err; 1117 | 1118 | if (!st) { 1119 | PyErr_SetString(PyExc_AssertionError, "module state is NULL"); 1120 | return NULL; 1121 | } 1122 | 1123 | if (!PyArg_ParseTupleAndKeywords(args, 1124 | kwargs, 1125 | "OO|p:namedtuple", 1126 | (char**) argnames, 1127 | &typename, 1128 | &field_names, 1129 | &rename)) { 1130 | return NULL; 1131 | } 1132 | 1133 | if (!(typename = PyObject_Str(typename))) { 1134 | /* Typename cannot be converted to `str`. */ 1135 | return NULL; 1136 | } 1137 | 1138 | /* Validate and rename the `typename` and `field_names`. After this 1139 | function, field_names` will point to a tuple of strings that is the 1140 | split and renamed fields (if there are no errors). */ 1141 | if (validate_field_names(typename, &field_names, rename, st->iskeyword)) { 1142 | /* Invalid `field_names` or `typename`. */ 1143 | Py_DECREF(typename); 1144 | return NULL; 1145 | } 1146 | 1147 | /* Lookup the module where this type was defined and store it as 1148 | `__module__` in the dict */ 1149 | if ((globals = PyEval_GetGlobals()) && 1150 | (module_name = PyDict_GetItemString(globals, "__name__"))) { 1151 | Py_INCREF(module_name); 1152 | } 1153 | /* Not defining a module is deprecated in >=3.5. We will set it to 1154 | "cnamedtuple.no_module" if we cannot find the module we are in. */ 1155 | else if (!(module_name = 1156 | PyUnicode_InternFromString("cnamedtuple.no_module"))){ 1157 | Py_DECREF(typename); 1158 | Py_DECREF(field_names); 1159 | return NULL; 1160 | } 1161 | 1162 | qualname = PyUnicode_FromFormat("%S.%U", module_name, typename); 1163 | Py_DECREF(typename); 1164 | Py_DECREF(module_name); 1165 | if (!qualname) { 1166 | Py_DECREF(field_names); 1167 | return NULL; 1168 | } 1169 | 1170 | /* We need to play with the memory around namedtuple_spec.name because 1171 | PyType_FromSpec assumes that the name is statically allocated. 1172 | Here we will point the name at the data for our qualname unicode object. 1173 | The name field now shares a lifetime with qualname because it points to 1174 | the same data. */ 1175 | if (!(namedtuple_spec.name = PyUnicode_AsUTF8(qualname))) { 1176 | Py_DECREF(qualname); 1177 | Py_DECREF(field_names); 1178 | return NULL; 1179 | } 1180 | newtype = (PyTypeObject*) PyType_FromSpec(&namedtuple_spec); 1181 | namedtuple_spec.name = ""; /* set back to our sentinel */ 1182 | Py_DECREF(qualname); /* kill the qualname */ 1183 | if (!newtype) { 1184 | Py_DECREF(field_names); 1185 | return NULL; 1186 | } 1187 | /* make tp_name point to the memory allocated for ht_name so that it 1188 | shares a lifetime with the ht_name field. */ 1189 | if (!(newtype->tp_name = 1190 | PyUnicode_AsUTF8(((PyHeapTypeObject*) newtype)->ht_name))) { 1191 | Py_DECREF(field_names); 1192 | Py_DECREF(newtype); 1193 | return NULL; 1194 | } 1195 | 1196 | dict_ = newtype->tp_dict; 1197 | 1198 | /* Add indexers for each name. */ 1199 | if (add_indexers(dict_, field_names)) { 1200 | Py_DECREF(field_names); 1201 | Py_DECREF(newtype); 1202 | return NULL; 1203 | } 1204 | 1205 | /* Add the field descriptor. */ 1206 | if (!(descr = PyObject_New(namedtuple_descr_wrapper, 1207 | &namedtuple_descr_wrapper_type))) { 1208 | Py_DECREF(field_names); 1209 | Py_DECREF(newtype); 1210 | return NULL; 1211 | } 1212 | descr->wr_wrapped = field_names; 1213 | err = PyDict_SetItemString(dict_, "_fields", (PyObject*) descr); 1214 | Py_DECREF(descr); 1215 | if (err) { 1216 | Py_DECREF(newtype); 1217 | return NULL; 1218 | } 1219 | 1220 | /* Add an empty '__slots__' */ 1221 | if (!(descr = PyObject_New(namedtuple_descr_wrapper, 1222 | &namedtuple_descr_wrapper_type))) { 1223 | Py_DECREF(newtype); 1224 | return NULL; 1225 | } 1226 | if (!(descr->wr_wrapped = PyTuple_New(0))) { 1227 | Py_DECREF(newtype); 1228 | return NULL; 1229 | } 1230 | err = PyDict_SetItemString(dict_, "__slots__", (PyObject*) descr); 1231 | Py_DECREF(descr); 1232 | if (err) { 1233 | Py_DECREF(newtype); 1234 | return NULL; 1235 | } 1236 | 1237 | /* Add the `__reprfmt__` and `tp_doc`.*/ 1238 | if (cache_repr_fmt(dict_, field_names)) { 1239 | Py_DECREF(newtype); 1240 | return NULL; 1241 | } 1242 | 1243 | /* set the asdict constructor */ 1244 | if (PyDict_SetItemString(dict_, "__asdict__", st->asdict)) { 1245 | Py_DECREF(newtype); 1246 | return NULL; 1247 | } 1248 | 1249 | return (PyObject*) newtype; 1250 | } 1251 | 1252 | static PyObject * 1253 | _register_asdict(PyObject *self, PyObject *asdict) 1254 | { 1255 | module_state *st = PyModule_GetState(self); 1256 | 1257 | Py_DECREF(st->asdict); 1258 | Py_INCREF(asdict); 1259 | st->asdict = asdict; 1260 | 1261 | Py_RETURN_NONE; 1262 | } 1263 | 1264 | PyDoc_STRVAR(namedtuple_doc, 1265 | "Returns a new subclass of tuple with named fields.\n" 1266 | "\n" 1267 | " >>> Point = namedtuple('Point', ['x', 'y'])\n" 1268 | " 'Point(x, y)'\n" 1269 | " >>> p = Point(11, y=22) # instantiate with positional args or keywords\n" 1270 | " >>> p[0] + p[1] # indexable like a plain tuple\n" 1271 | " 33\n" 1272 | " >>> x, y = p # unpack like a regular tuple\n" 1273 | " >>> x, y\n" 1274 | " (11, 22)\n" 1275 | " >>> p.x + p.y # fields also accessable by name\n" 1276 | " 33\n" 1277 | " >>> d = p._asdict() # convert to a dictionary\n" 1278 | " >>> d['x']\n" 1279 | " 11\n" 1280 | " >>> Point(**d) # convert from a dictionary\n" 1281 | " Point(x=11, y=22)\n" 1282 | " >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields\n" 1283 | " Point(x=100, y=22)\n"); 1284 | 1285 | PyDoc_STRVAR(_register_asdict_doc, 1286 | "_register_asdict(type) -> None\n\n" 1287 | "Register the type constructor to use in the '_asdict' method for nametuple.\n" 1288 | "This is part of the internal api to solve circular dependencies."); 1289 | 1290 | 1291 | // The module level methods for the `namedtuple` module. 1292 | PyMethodDef _namedtuple_methods[] = { 1293 | {.ml_name="namedtuple", 1294 | .ml_meth=(PyCFunction) namedtuple_factory, 1295 | .ml_flags=METH_VARARGS | METH_KEYWORDS, 1296 | .ml_doc=namedtuple_doc}, 1297 | {.ml_name="_register_asdict", 1298 | .ml_meth=_register_asdict, 1299 | .ml_flags=METH_O, 1300 | .ml_doc=_register_asdict_doc}, 1301 | {NULL}, 1302 | }; 1303 | 1304 | static int 1305 | module_traverse(PyObject *self,visitproc visit,void *arg) 1306 | { 1307 | module_state *st = PyModule_GetState(self); 1308 | 1309 | if (!st) { 1310 | return 1; 1311 | } 1312 | Py_VISIT(st->iskeyword); 1313 | Py_VISIT(st->asdict); 1314 | return 0; 1315 | } 1316 | 1317 | static int 1318 | module_clear(PyObject *self) { 1319 | module_state *st = PyModule_GetState(self); 1320 | 1321 | if (!st) { 1322 | return 1; 1323 | } 1324 | Py_CLEAR(st->iskeyword); 1325 | Py_CLEAR(st->asdict); 1326 | return 0; 1327 | } 1328 | 1329 | void 1330 | module_free(PyObject *self) { 1331 | module_state *st = PyModule_GetState(self); 1332 | 1333 | if (!st) { 1334 | return; 1335 | } 1336 | 1337 | Py_XDECREF(st->iskeyword); 1338 | Py_XDECREF(st->asdict); 1339 | } 1340 | 1341 | PyDoc_STRVAR(module_doc, 1342 | "High performance data structures.\n\ 1343 | - namedtuple: A factory for creating subclasses of tuple that can be \ 1344 | indexed by name\n\ 1345 | "); 1346 | 1347 | static struct PyMethodDef module_functions[] = { 1348 | {.ml_name="namedtuple", 1349 | .ml_meth=(PyCFunction) namedtuple_factory, 1350 | .ml_flags=METH_VARARGS | METH_KEYWORDS, 1351 | .ml_doc=namedtuple_doc}, 1352 | {.ml_name="_register_asdict", 1353 | .ml_meth=_register_asdict, 1354 | .ml_flags=METH_O, 1355 | .ml_doc=_register_asdict_doc}, 1356 | {NULL}, 1357 | }; 1358 | 1359 | static struct PyModuleDef _namedtuplemodule = { 1360 | PyModuleDef_HEAD_INIT, 1361 | "_namedtuple", 1362 | module_doc, 1363 | sizeof(module_state), 1364 | module_functions, 1365 | NULL, 1366 | module_traverse, 1367 | module_clear, 1368 | (freefunc) module_free, 1369 | }; 1370 | 1371 | PyMODINIT_FUNC 1372 | PyInit__namedtuple(void) 1373 | { 1374 | PyObject *m; 1375 | PyObject *keyword_mod; 1376 | module_state *st; 1377 | 1378 | if (PyType_Ready(&namedtuple_indexer_type) < 0) { 1379 | return NULL; 1380 | } 1381 | if (PyType_Ready(&namedtuple_descr_wrapper_type) < 0) { 1382 | return NULL; 1383 | } 1384 | 1385 | if (!(m = PyModule_Create(&_namedtuplemodule))) { 1386 | return NULL; 1387 | } 1388 | 1389 | if (!(st = PyModule_GetState(m))) { 1390 | PyErr_SetString(PyExc_SystemError,"Module state is NULL"); 1391 | Py_DECREF(m); 1392 | return NULL; 1393 | } 1394 | 1395 | st->asdict = (PyObject*) &PyDict_Type; 1396 | Py_INCREF(st->asdict); 1397 | 1398 | if (!(keyword_mod = PyImport_ImportModule("keyword"))) { 1399 | Py_DECREF(m); 1400 | return NULL; 1401 | } 1402 | st->iskeyword = PyObject_GetAttrString(keyword_mod, "iskeyword"); 1403 | Py_DECREF(keyword_mod); 1404 | if (!st->iskeyword) { 1405 | Py_DECREF(m); 1406 | return NULL; 1407 | } 1408 | 1409 | return m; 1410 | } 1411 | -------------------------------------------------------------------------------- /prof/bench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import os 4 | import re 5 | import statistics 6 | from string import ascii_letters 7 | import subprocess 8 | import sys 9 | 10 | import perf 11 | 12 | 13 | def argname(n): 14 | if not n: 15 | return ascii_letters[0] 16 | 17 | out = '' 18 | while n: 19 | n, ix = divmod(n, len(ascii_letters)) 20 | out += ascii_letters[ix] 21 | return out 22 | 23 | 24 | def test_type_creation_string(fields): 25 | 'type creation from string with %d field(s)' 26 | for n in range(1, fields + 1): 27 | yield ( 28 | '', 29 | "namedtuple('NT', '%s')" % ' '.join( 30 | map(argname, range(n)), 31 | ), 32 | n, 33 | ) 34 | 35 | 36 | def test_type_creation_sequence(fields): 37 | 'type creation from sequence with %d field(s)' 38 | for n in range(1, fields + 1): 39 | yield ( 40 | '', 41 | "namedtuple('NT', [%s])" % ', '.join( 42 | map(lambda a: repr(argname(a)), range(n)), 43 | ), 44 | n, 45 | ) 46 | 47 | 48 | def test_instance_creation_positional(fields): 49 | 'type instance creation with positional arguments and %d field(s)' 50 | for n in range(1, fields + 1): 51 | yield ( 52 | "NT = namedtuple('NT', [%s])" % ', '.join( 53 | map(lambda a: repr(argname(a)), range(n)), 54 | ), 55 | 'NT(%s)' % ', '.join(map(str, range(n))), 56 | n, 57 | ) 58 | 59 | 60 | def test_instance_creation_keyword(fields): 61 | 'type instance creation with keyword arguments and %d field(s)' 62 | for n in range(1, fields + 1): 63 | argnames = list(map(argname, range(n))) 64 | yield ( 65 | "NT = namedtuple('NT', [%s])" % ', '.join(map(repr, argnames)), 66 | 'NT(%s)' % ', '.join( 67 | '%s=%r' % (a, n) for n, a in enumerate(argnames) 68 | ), 69 | n, 70 | ) 71 | 72 | 73 | def test_field_access(fields): 74 | 'field access' 75 | yield ( 76 | "NT = namedtuple('NT', 'a b c');instance = NT(0, 1, 2)", 77 | 'instance.b', 78 | ) 79 | 80 | 81 | def run_test(test, fields): 82 | for setup, command, *args in test(fields): 83 | print(': '.join((test.__doc__ % tuple(args), command))) 84 | print(' collections: ', end='', flush=True) 85 | collections = timeit( 86 | command, 87 | setup='from collections import namedtuple;%s' % setup, 88 | ) 89 | print(collections.format()) 90 | print(' cnamedtuple: ', end='', flush=True) 91 | cnamedtuple = timeit( 92 | command, 93 | setup='from cnamedtuple import namedtuple;%s' % setup, 94 | ) 95 | print(cnamedtuple.format()) 96 | ratio = collections.median() / cnamedtuple.median() 97 | print(' ratio: %.2f\n' % ratio) 98 | yield ratio 99 | 100 | 101 | def timeit(command, *, setup=''): 102 | read, write = os.pipe() 103 | subprocess.run( 104 | [ 105 | sys.executable, 106 | '-m', 'perf', 107 | 'timeit', 108 | '-s', setup, 109 | '--pipe', str(write), 110 | command, 111 | ], 112 | pass_fds=[write], 113 | ) 114 | os.close(write) 115 | with open(read) as f: 116 | data = f.read() 117 | 118 | return perf.Benchmark.loads(data) 119 | 120 | 121 | def main(): 122 | parser = argparse.ArgumentParser('cnamedtuple benchmark suite') 123 | parser.add_argument( 124 | '-k', 125 | default='', 126 | help='Run tests matching this regex.', 127 | ) 128 | parser.add_argument( 129 | '-n', 130 | type=int, 131 | default=8, 132 | help='Run tests up to ``n`` fields.' 133 | ) 134 | 135 | args = parser.parse_args() 136 | pattern = re.compile(args.k) 137 | 138 | print('Running with: Python %s\n' % sys.version.replace('\n', '')) 139 | ratio = [] 140 | ns = globals().copy() 141 | for k, v in ns.items(): 142 | if k.startswith('test_') and pattern.match(k): 143 | ratio.extend(run_test(v, args.n)) 144 | 145 | if ratio: 146 | print('\nmedian ratio: %.2f' % statistics.median(ratio)) 147 | else: 148 | print('no tests selected') 149 | 150 | 151 | if __name__ == '__main__': 152 | main() 153 | -------------------------------------------------------------------------------- /prof/bench.out: -------------------------------------------------------------------------------- 1 | Running with: Python 3.6.1 (default, Mar 27 2017, 00:27:06) [GCC 6.3.1 20170306] 2 | 3 | type creation from string with 1 field(s): namedtuple('NT', 'a') 4 | collections: 253 us +- 10 us 5 | cnamedtuple: 6.47 us +- 0.38 us 6 | ratio: 39.03 7 | 8 | type creation from string with 2 field(s): namedtuple('NT', 'a b') 9 | collections: 271 us +- 8 us 10 | cnamedtuple: 6.59 us +- 0.17 us 11 | ratio: 41.18 12 | 13 | type creation from string with 3 field(s): namedtuple('NT', 'a b c') 14 | collections: 290 us +- 6 us 15 | cnamedtuple: 6.77 us +- 0.17 us 16 | ratio: 42.80 17 | 18 | type creation from string with 4 field(s): namedtuple('NT', 'a b c d') 19 | collections: 308 us +- 12 us 20 | cnamedtuple: 7.07 us +- 0.21 us 21 | ratio: 43.62 22 | 23 | type creation from string with 5 field(s): namedtuple('NT', 'a b c d e') 24 | collections: 322 us +- 5 us 25 | cnamedtuple: 7.38 us +- 0.18 us 26 | ratio: 43.66 27 | 28 | type creation from string with 6 field(s): namedtuple('NT', 'a b c d e f') 29 | collections: 337 us +- 4 us 30 | cnamedtuple: 7.67 us +- 0.26 us 31 | ratio: 43.92 32 | 33 | type creation from string with 7 field(s): namedtuple('NT', 'a b c d e f g') 34 | collections: 353 us +- 5 us 35 | cnamedtuple: 8.11 us +- 0.18 us 36 | ratio: 43.46 37 | 38 | type creation from string with 8 field(s): namedtuple('NT', 'a b c d e f g h') 39 | collections: 369 us +- 7 us 40 | cnamedtuple: 8.34 us +- 0.20 us 41 | ratio: 44.18 42 | 43 | type creation from sequence with 1 field(s): namedtuple('NT', ['a']) 44 | collections: 252 us +- 4 us 45 | cnamedtuple: 6.21 us +- 0.10 us 46 | ratio: 40.56 47 | 48 | type creation from sequence with 2 field(s): namedtuple('NT', ['a', 'b']) 49 | collections: 351 us +- 44 us 50 | cnamedtuple: 6.57 us +- 0.18 us 51 | ratio: 53.42 52 | 53 | type creation from sequence with 3 field(s): namedtuple('NT', ['a', 'b', 'c']) 54 | collections: 289 us +- 6 us 55 | cnamedtuple: 6.77 us +- 0.23 us 56 | ratio: 42.66 57 | 58 | type creation from sequence with 4 field(s): namedtuple('NT', ['a', 'b', 'c', 'd']) 59 | collections: 310 us +- 12 us 60 | cnamedtuple: 6.92 us +- 0.21 us 61 | ratio: 44.84 62 | 63 | type creation from sequence with 5 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e']) 64 | collections: 322 us +- 3 us 65 | cnamedtuple: 7.31 us +- 0.16 us 66 | ratio: 44.02 67 | 68 | type creation from sequence with 6 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e', 'f']) 69 | collections: 336 us +- 5 us 70 | cnamedtuple: 7.52 us +- 0.18 us 71 | ratio: 44.72 72 | 73 | type creation from sequence with 7 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e', 'f', 'g']) 74 | collections: 352 us +- 6 us 75 | cnamedtuple: 8.03 us +- 0.19 us 76 | ratio: 43.83 77 | 78 | type creation from sequence with 8 field(s): namedtuple('NT', ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']) 79 | collections: 366 us +- 4 us 80 | cnamedtuple: 8.25 us +- 0.18 us 81 | ratio: 44.38 82 | 83 | type instance creation with positional arguments and 1 field(s): NT(0) 84 | collections: 333 ns +- 8 ns 85 | cnamedtuple: 221 ns +- 5 ns 86 | ratio: 1.51 87 | 88 | type instance creation with positional arguments and 2 field(s): NT(0, 1) 89 | collections: 344 ns +- 10 ns 90 | cnamedtuple: 228 ns +- 5 ns 91 | ratio: 1.51 92 | 93 | type instance creation with positional arguments and 3 field(s): NT(0, 1, 2) 94 | collections: 355 ns +- 8 ns 95 | cnamedtuple: 234 ns +- 7 ns 96 | ratio: 1.51 97 | 98 | type instance creation with positional arguments and 4 field(s): NT(0, 1, 2, 3) 99 | collections: 362 ns +- 6 ns 100 | cnamedtuple: 242 ns +- 5 ns 101 | ratio: 1.49 102 | 103 | type instance creation with positional arguments and 5 field(s): NT(0, 1, 2, 3, 4) 104 | collections: 378 ns +- 14 ns 105 | cnamedtuple: 249 ns +- 8 ns 106 | ratio: 1.52 107 | 108 | type instance creation with positional arguments and 6 field(s): NT(0, 1, 2, 3, 4, 5) 109 | collections: 382 ns +- 9 ns 110 | cnamedtuple: 254 ns +- 6 ns 111 | ratio: 1.51 112 | 113 | type instance creation with positional arguments and 7 field(s): NT(0, 1, 2, 3, 4, 5, 6) 114 | collections: 397 ns +- 10 ns 115 | cnamedtuple: 262 ns +- 11 ns 116 | ratio: 1.52 117 | 118 | type instance creation with positional arguments and 8 field(s): NT(0, 1, 2, 3, 4, 5, 6, 7) 119 | collections: 422 ns +- 11 ns 120 | cnamedtuple: 269 ns +- 5 ns 121 | ratio: 1.57 122 | 123 | type instance creation with keyword arguments and 1 field(s): NT(a=0) 124 | collections: 421 ns +- 7 ns 125 | cnamedtuple: 269 ns +- 5 ns 126 | ratio: 1.56 127 | 128 | type instance creation with keyword arguments and 2 field(s): NT(a=0, b=1) 129 | collections: 455 ns +- 7 ns 130 | cnamedtuple: 300 ns +- 6 ns 131 | ratio: 1.52 132 | 133 | type instance creation with keyword arguments and 3 field(s): NT(a=0, b=1, c=2) 134 | collections: 491 ns +- 15 ns 135 | cnamedtuple: 330 ns +- 12 ns 136 | ratio: 1.49 137 | 138 | type instance creation with keyword arguments and 4 field(s): NT(a=0, b=1, c=2, d=3) 139 | collections: 526 ns +- 11 ns 140 | cnamedtuple: 357 ns +- 6 ns 141 | ratio: 1.47 142 | 143 | type instance creation with keyword arguments and 5 field(s): NT(a=0, b=1, c=2, d=3, e=4) 144 | collections: 572 ns +- 9 ns 145 | cnamedtuple: 388 ns +- 11 ns 146 | ratio: 1.47 147 | 148 | type instance creation with keyword arguments and 6 field(s): NT(a=0, b=1, c=2, d=3, e=4, f=5) 149 | collections: 666 ns +- 20 ns 150 | cnamedtuple: 469 ns +- 15 ns 151 | ratio: 1.42 152 | 153 | type instance creation with keyword arguments and 7 field(s): NT(a=0, b=1, c=2, d=3, e=4, f=5, g=6) 154 | collections: 698 ns +- 17 ns 155 | cnamedtuple: 493 ns +- 11 ns 156 | ratio: 1.42 157 | 158 | type instance creation with keyword arguments and 8 field(s): NT(a=0, b=1, c=2, d=3, e=4, f=5, g=6, h=7) 159 | collections: 741 ns +- 15 ns 160 | cnamedtuple: 526 ns +- 17 ns 161 | ratio: 1.41 162 | 163 | field access: instance.b 164 | collections: 45.2 ns +- 1.2 ns 165 | cnamedtuple: 24.8 ns +- 0.8 ns 166 | ratio: 1.82 167 | 168 | 169 | median ratio: 1.82 170 | -------------------------------------------------------------------------------- /prof/instance_creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llllllllll/cnamedtuple/7dc913bca3985ac5a2997ae6dc651f21527a7fca/prof/instance_creation.png -------------------------------------------------------------------------------- /prof/type_creation_seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llllllllll/cnamedtuple/7dc913bca3985ac5a2997ae6dc651f21527a7fca/prof/type_creation_seq.png -------------------------------------------------------------------------------- /prof/type_creation_string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llllllllll/cnamedtuple/7dc913bca3985ac5a2997ae6dc651f21527a7fca/prof/type_creation_string.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | import sys 3 | 4 | long_description = None 5 | 6 | if 'upload' in sys.argv: 7 | with open('README.rst') as readme: 8 | long_description = readme.read() 9 | 10 | setup( 11 | name='cnamedtuple', 12 | version='0.1.6', 13 | description='collections.namedtuple implemented in c.', 14 | author='Joe Jevnik', 15 | author_email='joe@quantopian.com', 16 | packages=[ 17 | 'cnamedtuple', 18 | ], 19 | long_description=long_description, 20 | license='Apache 2.0', 21 | classifiers=[ 22 | 'Development Status :: 4 - Beta', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: Apache Software License', 25 | 'Natural Language :: English', 26 | 'Programming Language :: Python :: 3 :: Only', 27 | 'Programming Language :: Python :: Implementation :: CPython', 28 | 'Operating System :: OS Independent', 29 | 'Topic :: Utilities', 30 | ], 31 | url="https://github.com/llllllllll/cnamedtuple", 32 | ext_modules=[ 33 | Extension( 34 | 'cnamedtuple._namedtuple', 35 | ['cnamedtuple/_namedtuple.c'], 36 | extra_compile_args=[ 37 | '-Wall', 38 | '-Wextra', 39 | '-Wno-unused-parameter', 40 | '-Wno-missing-field-initializers', 41 | ], 42 | ), 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /tests/test_namedtuple.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2 | # 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights 3 | # Reserved 4 | # This file is taken from the CPython test suite and modified to test 5 | # cnamedtuple instead of collections.namedtuple 6 | from collections import OrderedDict 7 | import copy 8 | import pickle 9 | from random import choice 10 | import string 11 | import sys 12 | import unittest 13 | 14 | from cnamedtuple import namedtuple 15 | 16 | 17 | TestNT = namedtuple('TestNT', 'x y z') # type used for pickle tests 18 | 19 | 20 | class TestNamedTuple(unittest.TestCase): 21 | 22 | def test_factory(self): 23 | Point = namedtuple('Point', 'x y') 24 | self.assertEqual(Point.__name__, 'Point') 25 | self.assertEqual(Point.__slots__, ()) 26 | self.assertEqual(Point.__module__, __name__) 27 | self.assertEqual(Point.__getitem__, tuple.__getitem__) 28 | self.assertEqual(Point._fields, ('x', 'y')) 29 | 30 | self.assertRaises(ValueError, namedtuple, 'abc%', 'efg ghi') # type has non-alpha char 31 | self.assertRaises(ValueError, namedtuple, 'class', 'efg ghi') # type has keyword 32 | self.assertRaises(ValueError, namedtuple, '9abc', 'efg ghi') # type starts with digit 33 | 34 | self.assertRaises(ValueError, namedtuple, 'abc', 'efg g%hi') # field with non-alpha char 35 | self.assertRaises(ValueError, namedtuple, 'abc', 'abc class') # field has keyword 36 | self.assertRaises(ValueError, namedtuple, 'abc', '8efg 9ghi') # field starts with digit 37 | self.assertRaises(ValueError, namedtuple, 'abc', '_efg ghi') # field with leading underscore 38 | self.assertRaises(ValueError, namedtuple, 'abc', 'efg efg ghi') # duplicate field 39 | self.assertRaises(ValueError, namedtuple, 'abc', '?a') # non alnum starting char 40 | 41 | namedtuple('Point0', 'x1 y2') # Verify that numbers are allowed in names 42 | namedtuple('_', 'a b c') # Test leading underscores in a typename 43 | 44 | nt = namedtuple('nt', 'the quick brown fox') # check unicode input 45 | self.assertNotIn("u'", repr(nt._fields)) 46 | nt = namedtuple('nt', ('the', 'quick')) # check unicode input 47 | self.assertNotIn("u'", repr(nt._fields)) 48 | 49 | self.assertRaises(TypeError, Point._make, [11]) # catch too few args 50 | self.assertRaises(TypeError, Point._make, [11, 22, 33]) # catch too many args 51 | 52 | def test_name_fixer(self): 53 | for spec, renamed in [ 54 | [('efg', 'g%hi'), ('efg', '_1')], # field with non-alpha char 55 | [('abc', 'class'), ('abc', '_1')], # field has keyword 56 | [('8efg', '9ghi'), ('_0', '_1')], # field starts with digit 57 | [('abc', '_efg'), ('abc', '_1')], # field with leading underscore 58 | [('abc', 'efg', 'efg', 'ghi'), ('abc', 'efg', '_2', 'ghi')], # duplicate field 59 | [('abc', '', 'x'), ('abc', '_1', 'x')], # fieldname is a space 60 | ]: 61 | self.assertEqual(namedtuple('NT', spec, rename=True)._fields, renamed) 62 | 63 | def test_instance(self): 64 | Point = namedtuple('Point', 'x y') 65 | p = Point(11, 22) 66 | self.assertEqual(p, Point(x=11, y=22)) 67 | self.assertEqual(p, Point(11, y=22)) 68 | self.assertEqual(p, Point(y=22, x=11)) 69 | self.assertEqual(p, Point(*(11, 22))) 70 | self.assertEqual(p, Point(**dict(x=11, y=22))) 71 | self.assertRaises(TypeError, Point, 1) # too few args 72 | self.assertRaises(TypeError, Point, 1, 2, 3) # too many args 73 | self.assertRaises(TypeError, eval, 'Point(XXX=1, y=2)', locals()) # wrong keyword argument 74 | self.assertRaises(TypeError, eval, 'Point(x=1)', locals()) # missing keyword argument 75 | self.assertEqual(repr(p), 'Point(x=11, y=22)') 76 | self.assertNotIn('__weakref__', dir(p)) 77 | self.assertEqual(p, Point._make([11, 22])) # test _make classmethod 78 | self.assertEqual(p._fields, ('x', 'y')) # test _fields attribute 79 | self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method 80 | self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method 81 | 82 | try: 83 | p._replace(x=1, error=2) 84 | except ValueError: 85 | pass 86 | else: 87 | self._fail('Did not detect an incorrect fieldname') 88 | 89 | # verify that field string can have commas 90 | Point = namedtuple('Point', 'x, y') 91 | p = Point(x=11, y=22) 92 | self.assertEqual(repr(p), 'Point(x=11, y=22)') 93 | 94 | # verify that fieldspec can be a non-string sequence 95 | Point = namedtuple('Point', ('x', 'y')) 96 | p = Point(x=11, y=22) 97 | self.assertEqual(repr(p), 'Point(x=11, y=22)') 98 | 99 | def test_tupleness(self): 100 | Point = namedtuple('Point', 'x y') 101 | p = Point(11, 22) 102 | 103 | self.assertIsInstance(p, tuple) 104 | self.assertEqual(p, (11, 22)) # matches a real tuple 105 | self.assertEqual(tuple(p), (11, 22)) # coercable to a real tuple 106 | self.assertEqual(list(p), [11, 22]) # coercable to a list 107 | self.assertEqual(max(p), 22) # iterable 108 | self.assertEqual(max(*p), 22) # star-able 109 | x, y = p 110 | self.assertEqual(p, (x, y)) # unpacks like a tuple 111 | self.assertEqual((p[0], p[1]), (11, 22)) # indexable like a tuple 112 | self.assertRaises(IndexError, p.__getitem__, 3) 113 | 114 | self.assertEqual(p.x, x) 115 | self.assertEqual(p.y, y) 116 | self.assertRaises(AttributeError, eval, 'p.z', locals()) 117 | 118 | def test_odd_sizes(self): 119 | Zero = namedtuple('Zero', '') 120 | self.assertEqual(Zero(), ()) 121 | self.assertEqual(Zero._make([]), ()) 122 | self.assertEqual(repr(Zero()), 'Zero()') 123 | self.assertEqual(Zero()._asdict(), {}) 124 | self.assertEqual(Zero()._fields, ()) 125 | 126 | Dot = namedtuple('Dot', 'd') 127 | self.assertEqual(Dot(1), (1,)) 128 | self.assertEqual(Dot._make([1]), (1,)) 129 | self.assertEqual(Dot(1).d, 1) 130 | self.assertEqual(repr(Dot(1)), 'Dot(d=1)') 131 | self.assertEqual(Dot(1)._asdict(), {'d':1}) 132 | self.assertEqual(Dot(1)._replace(d=999), (999,)) 133 | self.assertEqual(Dot(1)._fields, ('d',)) 134 | 135 | # n = 5000 136 | n = 254 # SyntaxError: more than 255 arguments: 137 | names = list(set(''.join([choice(string.ascii_letters) 138 | for j in range(10)]) for i in range(n))) 139 | n = len(names) 140 | Big = namedtuple('Big', names) 141 | b = Big(*range(n)) 142 | self.assertEqual(b, tuple(range(n))) 143 | self.assertEqual(Big._make(range(n)), tuple(range(n))) 144 | for pos, name in enumerate(names): 145 | self.assertEqual(getattr(b, name), pos) 146 | repr(b) # make sure repr() doesn't blow-up 147 | d = b._asdict() 148 | d_expected = dict(zip(names, range(n))) 149 | self.assertEqual(d, d_expected) 150 | b2 = b._replace(**dict([(names[1], 999),(names[-5], 42)])) 151 | b2_expected = list(range(n)) 152 | b2_expected[1] = 999 153 | b2_expected[-5] = 42 154 | self.assertEqual(b2, tuple(b2_expected)) 155 | self.assertEqual(b._fields, tuple(names)) 156 | 157 | def test_pickle(self): 158 | p = TestNT(x=10, y=20, z=30) 159 | for module in (pickle,): 160 | loads = getattr(module, 'loads') 161 | dumps = getattr(module, 'dumps') 162 | for protocol in range(-1, module.HIGHEST_PROTOCOL + 1): 163 | q = loads(dumps(p, protocol)) 164 | self.assertEqual(p, q) 165 | self.assertEqual(p._fields, q._fields) 166 | self.assertNotIn(b'OrderedDict', dumps(p, protocol)) 167 | 168 | def test_copy(self): 169 | p = TestNT(x=10, y=20, z=30) 170 | for copier in copy.copy, copy.deepcopy: 171 | q = copier(p) 172 | self.assertEqual(p, q) 173 | self.assertEqual(p._fields, q._fields) 174 | 175 | def test_name_conflicts(self): 176 | # Some names like "self", "cls", "tuple", "itemgetter", and "property" 177 | # failed when used as field names. Test to make sure these now work. 178 | T = namedtuple('T', 'itemgetter property self cls tuple') 179 | t = T(1, 2, 3, 4, 5) 180 | self.assertEqual(t, (1,2,3,4,5)) 181 | newt = t._replace(itemgetter=10, property=20, self=30, cls=40, tuple=50) 182 | self.assertEqual(newt, (10,20,30,40,50)) 183 | 184 | def test_repr(self): 185 | A = namedtuple('A', 'x') 186 | self.assertEqual(repr(A(1)), 'A(x=1)') 187 | # repr should show the name of the subclass 188 | class B(A): 189 | pass 190 | self.assertEqual(repr(B(1)), 'B(x=1)') 191 | 192 | 193 | def test_namedtuple_subclass_issue_24931(self): 194 | class Point(namedtuple('_Point', ['x', 'y'])): 195 | pass 196 | 197 | a = Point(3, 4) 198 | self.assertEqual(a._asdict(), OrderedDict([('x', 3), ('y', 4)])) 199 | 200 | a.w = 5 201 | self.assertEqual(a.__dict__, {'w': 5}) 202 | --------------------------------------------------------------------------------