├── .github
└── workflows
│ └── python-publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── README_CN.md
├── requirements.txt
├── setup.py
└── systempath
├── __init__.py
└── i systempath.py
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | permissions:
8 | contents: read
9 |
10 | jobs:
11 | deploy:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Set up Python
16 | uses: actions/setup-python@v3
17 | with:
18 | python-version: '3.x'
19 | - name: Install dependencies
20 | run: |
21 | python -m pip install --upgrade pip
22 | pip install build -r requirements.txt
23 | - name: Build package
24 | run: python -m build
25 | - name: Publish package
26 | uses: pypa/gh-action-pypi-publish@master
27 | with:
28 | user: __token__
29 | password: ${{ secrets.PYPI_API_TOKEN }}
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.pyc
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | env/
13 | py2env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | .mypy_cache/
29 | .idea/
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *,cover
50 | .hypothesis/
51 | .pytest_cache/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 |
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # IPython Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # dotenv
85 | .env
86 |
87 | # virtualenv
88 | .venv/
89 | venv/
90 | ENV/
91 |
92 | # Spyder project settings
93 | .spyderproject
94 |
95 | # Rope project settings
96 | .ropeproject
97 |
98 | # Mac
99 | .DS_Store
100 |
101 | # vim
102 | *.swp
103 |
104 | # netCDF Files
105 | *.nc
106 | conda-requirements.txt
107 |
108 | tests/
109 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [
](http://gqylpy.com)
2 | [](https://github.com/gqylpy/systempath/releases/latest)
3 | [](https://pypi.org/project/systempath)
4 | [](https://github.com/gqylpy/systempath/blob/master/LICENSE)
5 | [](https://pepy.tech/project/systempath)
6 |
7 | # systempath
8 | English | [中文](https://github.com/gqylpy/systempath/blob/master/README_CN.md)
9 |
10 | **systempath** is a highly specialized library designed for Python developers for file and system path manipulation. By providing an intuitive and powerful object-oriented API, it significantly simplifies complex file and directory management tasks, allowing developers to focus more on implementing core business logic rather than the intricacies of low-level file system operations.
11 |
12 | pip3 install systempath
13 |
14 | ```python
15 | >>> from systempath import SystemPath, Directory, File
16 |
17 | >>> root = SystemPath('/')
18 |
19 | >>> home: Directory = root['home']['gqylpy']
20 | >>> home
21 | /home/gqylpy
22 |
23 | >>> file: File = home['alpha.txt']
24 | >>> file
25 | /home/gqylpy/alpha.txt
26 |
27 | >>> file.content
28 | b'GQYLPY \xe6\x94\xb9\xe5\x8f\x98\xe4\xb8\x96\xe7\x95\x8c'
29 | ```
30 |
31 | ## Core Features
32 |
33 | ### 1. Object-Oriented Path Representation
34 |
35 | - **Directory Class**: Specifically designed to represent directory paths, providing directory-specific operations such as traversal, creation, deletion, and management of subdirectories and files.
36 | - **File Class**: Specifically designed to represent file paths, offering advanced functions beyond basic file operations, including content reading and writing, appending, and clearing.
37 | - **SystemPath Class**: Serves as a universal interface for `Directory` and `File`, providing maximum flexibility to handle any type of path, whether it's a file or directory.
38 |
39 | ### 2. Automation and Flexibility
40 |
41 | - **Automatic Absolute Path Conversion**: Supports automatically converting relative paths to absolute paths during path object initialization, reducing issues caused by incorrect paths.
42 | - **Strict Mode**: Allows developers to enable strict mode, ensuring that paths do exist during initialization; otherwise, exceptions are thrown, enhancing code robustness and reliability.
43 |
44 | ### 3. Rich Operational Interfaces
45 |
46 | - **Path Concatenation**: Supports path concatenation using `/`, `+` operators, and even brackets, making path construction more intuitive and flexible.
47 | - **Comprehensive File and Directory Operations**: Provides a complete set of file and directory operation methods, including but not limited to reading, writing, copying, moving, deleting, and traversing, meeting various file processing needs.
48 |
49 | ## Usage Scenarios
50 |
51 | - **Automation Script Development**: In scenarios such as automated testing, deployment scripts, log management, systempath offers powerful file and directory manipulation capabilities, simplifying script writing processes.
52 | - **Web Application Development**: Handles user-uploaded files, generates temporary files, and more, making these operations simpler and more efficient with systempath.
53 | - **Data Science and Analysis**: When reading, writing, and processing data files stored in the file system, systempath provides a convenient file management approach for data scientists.
54 |
55 | ## Conclusion
56 |
57 | systempath is a comprehensive and easy-to-use library for file and system path manipulation. Through its object-oriented API design, it significantly simplifies the complexity of file and directory management in Python, allowing developers to focus more on implementing core business logic. Whether it's automation script development, web application building, or data science projects, systempath will be an indispensable and valuable assistant.
58 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | [
](http://gqylpy.com)
2 | [](https://github.com/gqylpy/systempath/releases/latest)
3 | [](https://pypi.org/project/systempath)
4 | [](https://github.com/gqylpy/systempath/blob/master/LICENSE)
5 | [](https://pepy.tech/project/systempath)
6 |
7 | # systempath - 专业级的文件与系统路径操作库
8 | [English](README.md) | 中文
9 |
10 | **systempath** 是一个专为Python开发者设计的,高度专业化的文件与系统路径操作库。通过提供一套直观且功能强大的面向对象API,它极大地简化了复杂文件与目录管理的任务,使开发者能够更专注于核心业务逻辑的实现,而非底层文件系统操作的细节。
11 |
12 | pip3 install systempath
13 |
14 | ```python
15 | >>> from systempath import SystemPath, Directory, File
16 |
17 | >>> root = SystemPath('/')
18 |
19 | >>> home: Directory = root['home']['gqylpy']
20 | >>> home
21 | /home/gqylpy
22 |
23 | >>> file: File = home['alpha.txt']
24 | >>> file
25 | /home/gqylpy/alpha.txt
26 |
27 | >>> file.content
28 | b'GQYLPY \xe6\x94\xb9\xe5\x8f\x98\xe4\xb8\x96\xe7\x95\x8c'
29 | ```
30 |
31 | ## 核心特性
32 |
33 | ### 1. 面向对象的路径表示
34 |
35 | - **Directory 类**:专门用于表示目录路径,提供目录遍历、创建、删除及子目录与文件管理等目录特定操作。
36 | - **File 类**:专门用于表示文件路径,除了基本的文件操作外,还提供了内容读写、追加、清空等高级功能。
37 | - **SystemPath 类**:作为 `Directory` 和 `File` 的通用接口,提供了最大的灵活性,能够处理任何类型的路径,无论是文件还是目录。
38 |
39 | ### 2. 自动化与灵活性
40 |
41 | - **自动绝对路径转换**:支持在路径对象初始化时自动将相对路径转换为绝对路径,减少因路径错误导致的问题。
42 | - **严格模式**:允许开发者启用严格模式,确保路径在初始化时确实存在,否则抛出异常,增强代码的健壮性和可靠性。
43 |
44 | ### 3. 丰富的操作接口
45 |
46 | - **路径拼接**:支持使用 `/` 和 `+` 操作符甚至是中括号进行路径拼接,使得路径构建更加直观和灵活。
47 | - **全面的文件与目录操作**:提供了一整套文件与目录操作方法,包括但不限于读取、写入、复制、移动、删除、遍历等,满足各种文件处理需求。
48 |
49 | ## 使用场景
50 |
51 | - **自动化脚本开发**:在自动化测试、部署脚本、日志管理等场景中,systempath 提供强大的文件与目录操作能力,能够简化脚本编写过程。
52 | - **Web应用开发**:处理用户上传的文件、生成临时文件等场景,systempath 使得这些操作更加简单高效。
53 | - **数据科学与分析**:读取、写入和处理存储在文件系统中的数据文件时,systempath 为数据科学家提供了便捷的文件管理方式。
54 |
55 | ## 结论
56 |
57 | systempath 是一个功能全面、易于使用的文件与系统路径操作库。通过其面向对象的API设计,它极大地简化了Python中文件与目录管理的复杂性,使得开发者能够更专注于核心业务逻辑的实现。无论是自动化脚本开发、Web应用构建,还是数据科学项目,systempath 都将是您不可或缺的得力助手。
58 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | exceptionx>=4.0,<5.0
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 | import systempath as i
3 | from systempath import File
4 |
5 | idoc: list = i.__doc__.split('\n')
6 |
7 | for index, line in enumerate(idoc):
8 | if line.startswith('@version: ', 4):
9 | version = line.split()[-1]
10 | break
11 | _, author, email = idoc[index + 1].split()
12 | source = idoc[index + 2].split()[-1]
13 |
14 | requires_file, install_requires = \
15 | File('requirements.txt') or File('systempath.egg-info/requires.txt'), []
16 |
17 | for require in requires_file:
18 | if not require:
19 | continue
20 | if require[0] == 91:
21 | break
22 | install_requires.append(require.decode())
23 |
24 | setuptools.setup(
25 | name=i.__name__,
26 | version=version,
27 | author=author,
28 | author_email=email,
29 | license='Apache 2.0',
30 | url='http://gqylpy.com',
31 | project_urls={'Source': source},
32 | description='''
33 | The `systempath` is a library designed for Python developers, providing
34 | intuitive and powerful APIs that simplify file and directory management
35 | tasks, allowing developers to focus more on core business logic.
36 | '''.strip().replace('\n ', ''),
37 | long_description=File('README.md').content.decode('utf-8'),
38 | long_description_content_type='text/markdown',
39 | packages=[i.__name__],
40 | python_requires='>=3.8',
41 | install_requires=install_requires,
42 | extras_require={'pyyaml': ['PyYAML>=6.0,<7.0']},
43 | classifiers=[
44 | 'Development Status :: 4 - Beta',
45 | 'Intended Audience :: Developers',
46 | 'Intended Audience :: System Administrators',
47 | 'License :: OSI Approved :: Apache Software License',
48 | 'Natural Language :: Chinese (Simplified)',
49 | 'Natural Language :: English',
50 | 'Operating System :: OS Independent',
51 | 'Topic :: Software Development :: Libraries :: Python Modules',
52 | 'Topic :: System',
53 | 'Topic :: System :: Filesystems',
54 | 'Topic :: System :: Operating System',
55 | 'Topic :: System :: Operating System Kernels :: Linux',
56 | 'Topic :: System :: Systems Administration :: Authentication/Directory '
57 | ':: LDAP',
58 | 'Programming Language :: Python :: 3.8',
59 | 'Programming Language :: Python :: 3.9',
60 | 'Programming Language :: Python :: 3.10',
61 | 'Programming Language :: Python :: 3.11',
62 | 'Programming Language :: Python :: 3.12',
63 | 'Programming Language :: Python :: 3.13'
64 | ]
65 | )
66 |
--------------------------------------------------------------------------------
/systempath/__init__.py:
--------------------------------------------------------------------------------
1 | """A Professional Library for File and System Path Manipulation
2 |
3 | The `systempath` is a highly specialized library designed for Python developers
4 | forfile and system path manipulation. By providing an intuitive and powerful
5 | object-oriented API, it significantly simplifies complex file and directory
6 | management tasks, allowing developers to focus more on implementing core
7 | business logic rather than the intricacies of low-level file system operations.
8 |
9 | >>> from systempath import SystemPath, Directory, File
10 |
11 | >>> root = SystemPath('/')
12 |
13 | >>> home: Directory = root['home']['gqylpy']
14 | >>> home
15 | /home/gqylpy
16 |
17 | >>> file: File = home['alpha.txt']
18 | >>> file
19 | /home/gqylpy/alpha.txt
20 |
21 | >>> file.content
22 | b'GQYLPY \xe6\x94\xb9\xe5\x8f\x98\xe4\xb8\x96\xe7\x95\x8c'
23 |
24 | ────────────────────────────────────────────────────────────────────────────────
25 | Copyright (c) 2022-2024 GQYLPY . All rights reserved.
26 |
27 | @version: 1.2.2
28 | @author: 竹永康
29 | @source: https://github.com/gqylpy/systempath
30 |
31 | Licensed under the Apache License, Version 2.0 (the "License");
32 | you may not use this file except in compliance with the License.
33 | You may obtain a copy of the License at
34 |
35 | https://www.apache.org/licenses/LICENSE-2.0
36 |
37 | Unless required by applicable law or agreed to in writing, software
38 | distributed under the License is distributed on an "AS IS" BASIS,
39 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
40 | See the License for the specific language governing permissions and
41 | limitations under the License.
42 | """
43 | import os
44 | import sys
45 | import typing
46 |
47 | from typing import (
48 | Type, TypeVar, Literal, Optional, Union, Dict, Tuple, List, Mapping,
49 | BinaryIO, TextIO, Callable, Sequence, Iterator, Iterable, Any
50 | )
51 |
52 | if typing.TYPE_CHECKING:
53 | import csv
54 | import json
55 | import yaml
56 | from _typeshed import SupportsWrite
57 | from configparser import ConfigParser, Interpolation
58 |
59 | if sys.version_info >= (3, 10):
60 | from typing import TypeAlias
61 | else:
62 | TypeAlias = TypeVar('TypeAlias')
63 |
64 | if sys.version_info >= (3, 11):
65 | from typing import Self
66 | else:
67 | Self = TypeVar('Self')
68 |
69 | __all__ = ['SystemPath', 'Path', 'Directory', 'File', 'Open', 'Content', 'tree']
70 |
71 | BytesOrStr: TypeAlias = TypeVar('BytesOrStr', bytes, str)
72 | PathLink: TypeAlias = BytesOrStr
73 | PathType: TypeAlias = Union['Path', 'Directory', 'File', 'SystemPath']
74 | FileOpener: TypeAlias = Callable[[PathLink, int], int]
75 | FileNewline: TypeAlias = Literal['', '\n', '\r', '\r\n']
76 | CopyFunction: TypeAlias = Callable[[PathLink, PathLink], None]
77 | CopyTreeIgnore: TypeAlias = \
78 | Callable[[PathLink, List[BytesOrStr]], List[BytesOrStr]]
79 |
80 | ConvertersMap: TypeAlias = Dict[str, Callable[[str], Any]]
81 | CSVDialectLike: TypeAlias = Union[str, 'csv.Dialect', Type['csv.Dialect']]
82 | JsonObjectHook: TypeAlias = Callable[[Dict[Any, Any]], Any]
83 | JsonObjectParse: TypeAlias = Callable[[str], Any]
84 | JsonObjectPairsHook: TypeAlias = Callable[[List[Tuple[Any, Any]]], Any]
85 | YamlDumpStyle: TypeAlias = Literal['|', '>', '|+', '>+']
86 |
87 | YamlLoader: TypeAlias = Union[
88 | Type['yaml.BaseLoader'], Type['yaml.Loader'], Type['yaml.FullLoader'],
89 | Type['yaml.SafeLoader'], Type['yaml.UnsafeLoader']
90 | ]
91 | YamlDumper: TypeAlias = Union[
92 | Type['yaml.BaseDumper'], Type['yaml.Dumper'], Type['yaml.SafeDumper']
93 | ]
94 |
95 | try:
96 | import exceptionx as ex
97 | except ModuleNotFoundError:
98 | if os.path.basename(sys.argv[0]) != 'setup.py':
99 | raise
100 | else:
101 | SystemPathNotFoundError: Type[ex.Error] = ex.SystemPathNotFoundError
102 | NotAPathError: Type[ex.Error] = ex.NotAPathError
103 | NotAFileError: Type[ex.Error] = ex.NotAFileError
104 | NotADirectoryOrFileError: Type[ex.Error] = ex.NotADirectoryOrFileError
105 | IsSameFileError: Type[ex.Error] = ex.IsSameFileError
106 |
107 |
108 | class CSVReader(Iterator[List[str]]):
109 | line_num: int
110 | @property
111 | def dialect(self) -> 'csv.Dialect': ...
112 | def __next__(self) -> List[str]: ...
113 |
114 |
115 | class CSVWriter:
116 | @property
117 | def dialect(self) -> 'csv.Dialect': ...
118 | def writerow(self, row: Iterable[Any]) -> Any: ...
119 | def writerows(self, rows: Iterable[Iterable[Any]]) -> None: ...
120 |
121 |
122 | class Path:
123 |
124 | def __init__(
125 | self,
126 | name: PathLink,
127 | /, *,
128 | autoabs: Optional[bool] = None,
129 | strict: Optional[bool] = None,
130 | dir_fd: Optional[int] = None,
131 | follow_symlinks: Optional[bool] = None
132 | ):
133 | """
134 | @param name
135 | A path link, hopefully absolute. If it is a relative path, the
136 | current working directory is used as the parent directory (the
137 | return value of `os.getcwd()`).
138 |
139 | @param autoabs
140 | Automatically normalize the path link and convert to absolute path,
141 | at initialization. The default is False. It is always recommended
142 | that you enable the parameter when the passed path is a relative
143 | path.
144 |
145 | @param strict
146 | Set to True to enable strict mode, which means that the passed path
147 | must exist, otherwise raise `SystemPathNotFoundError` (or other).
148 | The default is False.
149 |
150 | @param dir_fd
151 | This optional parameter applies only to the following methods:
152 | `readable`, `writeable`, `executable`, `rename`,
153 | `renames`, `replace`, `symlink`, `readlink`,
154 | `stat`, `lstat`, `chmod`, `access`,
155 | `chown`, `copy`, `copystat`, `copymode`,
156 | `lchmod`, `chflags`, `getxattr`, `listxattr`,
157 | `removexattr`, `link`, `unlink`, `mknod`,
158 | `copy`
159 |
160 | A file descriptor open to a directory, obtain by `os.open`, sample
161 | `os.open('dir/', os.O_RDONLY)`. If this parameter is specified and
162 | the parameter `path` is relative, the parameter `path` will then be
163 | relative to that directory; otherwise, this parameter is ignored.
164 |
165 | This parameter may not be available on your platform, using them
166 | will ignore or raise `NotImplementedError` if unavailable.
167 |
168 | @param follow_symlinks
169 | This optional parameter applies only to the following methods:
170 | `readable`, `writeable`, `executable`, `copystat`,
171 | `copymode`, `stat`, `chmod`, `access`,
172 | `chown`, `chflags`, `getxattr`, `setxattr`,
173 | `listxattr`, `removexattr`, `walk`, `copy`,
174 | `link`
175 |
176 | Used to indicate whether symbolic links are followed, the default is
177 | True. If specified as False, and the last element of the parameter
178 | `path` is a symbolic link, the action will point to the symbolic
179 | link itself, not to the path to which the link points.
180 |
181 | This parameter may not be available on your platform, using them
182 | will raise `NotImplementedError` if unavailable.
183 | """
184 | if strict and not os.path.exists(name):
185 | raise SystemPathNotFoundError
186 |
187 | self.name = os.path.abspath(name) if autoabs else name
188 | self.strict = strict
189 | self.dir_fd = dir_fd
190 | self.follow_symlinks = follow_symlinks
191 |
192 | def __bytes__(self) -> bytes:
193 | """Return the path of type bytes."""
194 |
195 | def __eq__(self, other: [PathType, PathLink], /) -> bool:
196 | """Return True if the absolute path of the path instance is equal to the
197 | absolute path of another path instance (can also be a path link
198 | character) else False."""
199 |
200 | def __len__(self) -> int:
201 | """Return the length of the path string (or bytes)."""
202 |
203 | def __bool__(self) -> bool:
204 | return self.exists
205 |
206 | def __fspath__(self) -> PathLink:
207 | return self.name
208 |
209 | def __truediv__(self, subpath: Union[PathType, PathLink], /) -> PathType:
210 | """
211 | Connect paths, where the path on the right can be an instance of `path`
212 | or a string representing a path link. Return a new connected path
213 | instance whose properties are inherited from the left path.
214 |
215 | When `self.strict` is set to True, an exact instance of a directory or
216 | file is returned. Otherwise, an instance of `SystemPath` is generally
217 | returned.
218 | """
219 |
220 | def __rtruediv__(self, dirpath: PathLink, /) -> PathType:
221 | """Connect paths, where the path on the left is a path link string.
222 | Return a new connected path instance whose properties are inherited from
223 | the path on the right."""
224 |
225 | def __add__(self, subpath: Union[PathType, PathLink], /) -> PathType:
226 | return self / subpath
227 |
228 | def __radd__(self, dirpath: PathLink, /) -> PathType:
229 | return dirpath / self
230 |
231 | @property
232 | def basename(self) -> BytesOrStr:
233 | return os.path.basename(self)
234 |
235 | @property
236 | def dirname(self) -> 'Directory':
237 | return Directory(
238 | os.path.dirname(self),
239 | strict=self.strict,
240 | dir_fd=self.dir_fd,
241 | follow_symlinks=self.follow_symlinks
242 | )
243 |
244 | def dirnamel(self, level: int) -> 'Directory':
245 | """Like `self.dirname`, and can specify the directory level."""
246 | return Directory(
247 | self.name.rsplit(os.sep, maxsplit=level)[0],
248 | strict=self.strict,
249 | dir_fd=self.dir_fd,
250 | follow_symlinks=self.follow_symlinks
251 | )
252 |
253 | def ldirname(self, *, level: Optional[int] = None) -> PathType:
254 | """Cut the path from the left side, and can specify the cutting level
255 | through the parameter `level`, with a default of 1 level."""
256 |
257 | @property
258 | def abspath(self) -> PathType:
259 | return self.__class__(
260 | os.path.abspath(self),
261 | strict=self.strict,
262 | follow_symlinks=self.follow_symlinks
263 | )
264 |
265 | def realpath(self, *, strict: Optional[bool] = None) -> PathType:
266 | return self.__class__(
267 | os.path.realpath(self, strict=strict),
268 | strict=self.strict,
269 | follow_symlinks=self.follow_symlinks
270 | )
271 |
272 | def relpath(self, start: Optional[PathLink] = None) -> PathType:
273 | return self.__class__(
274 | os.path.relpath(self, start=start),
275 | strict=self.strict,
276 | follow_symlinks=self.follow_symlinks
277 | )
278 |
279 | def normpath(self) -> PathType:
280 | return self.__class__(
281 | os.path.normpath(self),
282 | strict=self.strict,
283 | dir_fd=self.dir_fd,
284 | follow_symlinks=self.follow_symlinks
285 | )
286 |
287 | def expanduser(self) -> PathType:
288 | return self.__class__(
289 | os.path.expanduser(self),
290 | strict=self.strict,
291 | follow_symlinks=self.follow_symlinks
292 | )
293 |
294 | def expandvars(self) -> PathType:
295 | return self.__class__(
296 | os.path.expandvars(self),
297 | strict=self.strict,
298 | follow_symlinks=self.follow_symlinks
299 | )
300 |
301 | def split(self) -> Tuple[PathLink, BytesOrStr]:
302 | return os.path.split(self)
303 |
304 | def splitdrive(self) -> Tuple[BytesOrStr, PathLink]:
305 | return os.path.splitdrive(self)
306 |
307 | @property
308 | def isabs(self) -> bool:
309 | return os.path.isabs(self)
310 |
311 | @property
312 | def exists(self) -> bool:
313 | return os.path.exists(self)
314 |
315 | @property
316 | def lexists(self) -> bool:
317 | """Like `self.exists`, but do not follow symbolic links, return True for
318 | broken symbolic links."""
319 | return os.path.lexists(self)
320 |
321 | @property
322 | def isdir(self) -> bool:
323 | return os.path.isdir(self)
324 |
325 | @property
326 | def isfile(self) -> bool:
327 | return os.path.isfile(self)
328 |
329 | @property
330 | def islink(self) -> bool:
331 | return os.path.islink(self)
332 |
333 | @property
334 | def ismount(self) -> bool:
335 | return os.path.ismount(self)
336 |
337 | @property
338 | def is_block_device(self) -> bool:
339 | """Return True if the path is a block device else False."""
340 |
341 | @property
342 | def is_char_device(self) -> bool:
343 | """Return True if the path is a character device else False."""
344 |
345 | @property
346 | def isfifo(self) -> bool:
347 | """Return True if the path is a FIFO else False."""
348 |
349 | @property
350 | def isempty(self) -> bool:
351 | """Return True if the directory (or the contents of the file) is empty
352 | else False. If the `self.name` is not a directory or file then raise
353 | `NotADirectoryOrFileError`."""
354 |
355 | @property
356 | def readable(self) -> bool:
357 | return self.access(os.R_OK)
358 |
359 | @property
360 | def writeable(self) -> bool:
361 | return self.access(os.W_OK)
362 |
363 | @property
364 | def executable(self) -> bool:
365 | return self.access(os.X_OK)
366 |
367 | def delete(
368 | self,
369 | *,
370 | ignore_errors: Optional[bool] = None,
371 | onerror: Optional[Callable] = None
372 | ) -> None:
373 | """
374 | Delete the path, if the path is a file then call `os.remove` internally,
375 | if the path is a directory call `shutil.rmtree` internally.
376 |
377 | @param ignore_errors
378 | If the path does not exist will raise `FileNotFoundError`, can set
379 | this parameter to True to silence the exception. The default is
380 | False.
381 |
382 | @param onerror
383 | An optional error handler, used only if the path is a directory, for
384 | more instructions see `shutil.rmtree`.
385 | """
386 |
387 | def rename(self, dst: PathLink, /) -> PathLink:
388 | """
389 | Rename the file or directory, call `os.rename` internally.
390 |
391 | The optional initialization parameter `self.dir_fd` will be passed to
392 | parameters `src_dir_fd` and `dst_dir_fd` of `os.rename`.
393 |
394 | Important Notice:
395 | If the destination path is relative and is a single name, the parent
396 | path of the source is used as the parent path of the destination instead
397 | of using the current working directory, different from the traditional
398 | way.
399 |
400 | Backstory about providing this method
401 | https://github.com/gqylpy/systempath/issues/1
402 |
403 | @return: The destination path.
404 | """
405 |
406 | def renames(self, dst: PathLink, /) -> PathLink:
407 | """
408 | Rename the file or directory, super version of `self.rename`. Call
409 | `os.renames` internally.
410 |
411 | When renaming, the destination path is created if the destination path
412 | does not exist, including any intermediate directories; After renaming,
413 | the source path is deleted if it is empty, delete from back to front
414 | until the entire path is used or a nonempty directory is found.
415 |
416 | Important Notice:
417 | If the destination path is relative and is a single name, the parent
418 | path of the source is used as the parent path of the destination instead
419 | of using the current working directory, different from the traditional
420 | way.
421 |
422 | Backstory about providing this method
423 | https://github.com/gqylpy/systempath/issues/1
424 |
425 | @return: The destination path.
426 | """
427 |
428 | def replace(self, dst: PathLink, /) -> PathLink:
429 | """
430 | Rename the file or directory, overwrite if destination exists. Call
431 | `os.replace` internally.
432 |
433 | The optional initialization parameter `self.dir_fd` will be passed to
434 | parameters `src_dir_fd` and `dst_dir_fd` of `os.replace`.
435 |
436 | Important Notice:
437 | If the destination path is relative and is a single name, the parent
438 | path of the source is used as the parent path of the destination instead
439 | of using the current working directory, different from the traditional
440 | way.
441 |
442 | Backstory about providing this method
443 | https://github.com/gqylpy/systempath/issues/1
444 |
445 | @return: The destination path.
446 | """
447 |
448 | def move(
449 | self,
450 | dst: Union[PathType, PathLink],
451 | /, *,
452 | copy_function: Optional[Callable[[PathLink, PathLink], None]] = None
453 | ) -> Union[PathType, PathLink]:
454 | """
455 | Move the file or directory to another location, similar to the Unix
456 | system `mv` command. Call `shutil.move` internally.
457 |
458 | @param dst:
459 | Where to move, hopefully pass in an instance of `Path`, can also
460 | pass in a path link.
461 |
462 | @param copy_function
463 | The optional parameter `copy_function` will be passed directly to
464 | `shutil.move` and default value is `shutil.copy2`.
465 |
466 | Backstory about providing this method
467 | https://github.com/gqylpy/systempath/issues/1
468 |
469 | @return: The parameter `dst` is passed in, without any modification.
470 | """
471 |
472 | def copystat(
473 | self, dst: Union[PathType, PathLink], /
474 | ) -> Union[PathType, PathLink]:
475 | """
476 | Copy the metadata of the file or directory to another file or directory,
477 | call `shutil.copystat` internally.
478 |
479 | @param dst:
480 | Where to copy the metadata, hopefully pass in an instance of `Path`,
481 | can also pass in a path link.
482 |
483 | The copied metadata includes permission bits, last access time, and last
484 | modification time. On Unix, extended attributes are also copied where
485 | possible. The file contents, owner, and group are not copied.
486 |
487 | If the optional initialization parameter `self.follow_symlinks` is
488 | specified as False, the action will point to the symbolic link itself,
489 | not to the path to which the link points, if and only if both the
490 | initialization parameter `self.name` and the parameter `dst` are
491 | symbolic links.
492 |
493 | @return: The parameter `dst` is passed in, without any modification.
494 | """
495 |
496 | def copymode(
497 | self, dst: Union[PathType, PathLink], /
498 | ) -> Union[PathType, PathLink]:
499 | """
500 | Copy the mode bits of the file or directory to another file or
501 | directory, call `shutil.copymode` internally.
502 |
503 | @param dst:
504 | Where to copy the mode bits, hopefully pass in an instance of
505 | `Path`, can also pass in a path link.
506 |
507 | If the optional initialization parameter `self.follow_symlinks` is
508 | specified as False, the action will point to the symbolic link itself,
509 | not to the path to which the link points, if and only if both the
510 | initialization parameter `self.name` and the parameter `dst` are
511 | symbolic links. But if `self.lchmod` isn't available (e.g. Linux) this
512 | method does nothing.
513 |
514 | @return: The parameter `dst` is passed in, without any modification.
515 | """
516 |
517 | def symlink(
518 | self, dst: Union[PathType, PathLink], /
519 | ) -> Union[PathType, PathLink]:
520 | """
521 | Create a symbolic link to the file or directory, call `os.symlink`
522 | internally.
523 |
524 | @param dst:
525 | Where to create the symbolic link, hopefully pass in an instance of
526 | `Path`, can also pass in a path link.
527 |
528 | @return: The parameter `dst` is passed in, without any modification.
529 | """
530 |
531 | def readlink(self) -> PathLink:
532 | """
533 | Return the path to which the symbolic link points.
534 |
535 | If the initialization parameter `self.name` is not a symbolic link, call
536 | this method will raise `OSError`.
537 | """
538 |
539 | @property
540 | def stat(self) -> os.stat_result:
541 | """
542 | Get the status of the file or directory, perform a stat system call
543 | against the file or directory. Call `os.stat` internally.
544 |
545 | If the optional initialization parameter `self.follow_symlinks` is
546 | specified as False, and the last element of the path is a symbolic link,
547 | the action will point to the symbolic link itself, not to the path to
548 | which the link points.
549 |
550 | @return: os.stat_result(
551 | st_mode: int = access mode,
552 | st_ino: int = inode number,
553 | st_dev: int = device number,
554 | st_nlink: int = number of hard links,
555 | st_uid: int = user ID of owner,
556 | st_gid: int = group ID of owner,
557 | st_size: int = total size in bytes,
558 | st_atime: float = time of last access,
559 | st_mtime: float = time of last modification,
560 | st_ctime: float = time of last change (on Unix) or created (on
561 | Windows)
562 | ...
563 | More attributes, you can look up `os.stat_result`.
564 | )
565 | """
566 |
567 | @property
568 | def lstat(self) -> os.stat_result:
569 | """Get the status of the file or directory, like `self.stat`, but do not
570 | follow symbolic links."""
571 | return self.__class__(
572 | self.name, dir_fd=self.dir_fd, follow_symlinks=False
573 | ).stat
574 |
575 | def getsize(self) -> int:
576 | """Get the size of the file, return 0 if the path is a directory."""
577 | return os.path.getsize(self)
578 |
579 | def getctime(self) -> float:
580 | return os.path.getctime(self)
581 |
582 | def getmtime(self) -> float:
583 | return os.path.getmtime(self)
584 |
585 | def getatime(self) -> float:
586 | return os.path.getatime(self)
587 |
588 | def chmod(self, mode: int, /) -> None:
589 | """
590 | Change the access permissions of the file or directory, call `os.chmod`
591 | internally.
592 |
593 | @param mode
594 | Specify the access permissions, can be a permission mask (0o600),
595 | can be a combination (0o600|stat.S_IFREG), can be a bitwise (33152).
596 |
597 | If the optional initialization parameter `self.follow_symlinks` is
598 | specified as False, and the last element of the path is a symbolic link,
599 | the action will point to the symbolic link itself, not to the path to
600 | which the link points.
601 | """
602 |
603 | def access(
604 | self, mode: int, /, *, effective_ids: Optional[bool] = None
605 | ) -> bool:
606 | """
607 | Test access permissions to the path using the real uid/gid, call
608 | `os.access` internally.
609 |
610 | @param mode
611 | Test which access permissions, can be the inclusive-or(`|`) of:
612 | `os.R_OK`: real value is 4, whether readable.
613 | `os.W_OK`: real value is 2, whether writeable.
614 | `os.X_OK`: real value is 1, whether executable.
615 | `os.F_OK`: real value is 0, whether exists.
616 |
617 | @param effective_ids
618 | The default is False, this parameter may not be available on your
619 | platform, using them will ignore if unavailable. You can look up
620 | `os.access` for more description.
621 |
622 | If the optional initialization parameter `self.follow_symlinks` is
623 | specified as False, and the last element of the path is a symbolic link,
624 | the action will point to the symbolic link itself, not to the path to
625 | which the link points.
626 | """
627 |
628 | if sys.platform != 'win32':
629 | def lchmod(self, mode: int, /) -> None:
630 | """Change the access permissions of the file or directory, like
631 | `self.chmod`, but do not follow symbolic links."""
632 | self.__class__(self.name, follow_symlinks=False).chmod(mode)
633 |
634 | @property
635 | def owner(self) -> str:
636 | """Get the login name of the path owner."""
637 |
638 | @property
639 | def group(self) -> str:
640 | """Get the group name of the path owner group."""
641 |
642 | def chown(self, uid: int, gid: int) -> None:
643 | """
644 | Change the owner and owner group of the file or directory, call
645 | `os.chown` internally.
646 |
647 | @param uid
648 | Specify the owner id, obtain by `os.getuid()`.
649 |
650 | @param gid
651 | Specify the owner group id, obtain by `os.getgid()`.
652 |
653 | If the optional initialization parameter `self.follow_symlinks` is
654 | specified as False, and the last element of the path is a symbolic
655 | link, the action will point to the symbolic link itself, not to the
656 | path to which the link points.
657 | """
658 |
659 | def lchown(self, uid: int, gid: int) -> None:
660 | """Change the owner and owner group of the file or directory, like
661 | `self.chown`, but do not follow symbolic links."""
662 | self.__class__(self.name, follow_symlinks=False).chown(uid, gid)
663 |
664 | def chflags(self, flags: int) -> None:
665 | """"
666 | Set the flag for the file or directory, different flag have
667 | different attributes. Call `os.chflags` internally.
668 |
669 | @param flags
670 | Specify numeric flag, can be the inclusive-or(`|`) of:
671 | `stat.UF_NODUMP`:
672 | do not dump file
673 | real value is 0x00000001 (1)
674 | `stat.UF_IMMUTABLE`:
675 | file may not be changed
676 | real value is 0x00000002 (2)
677 | `stat.UF_APPEND`:
678 | file may only be appended to
679 | real value is 0x00000004 (4)
680 | `stat.UF_OPAQUE`:
681 | directory is opaque when viewed through a union stack
682 | real value is 0x00000008 (8)
683 | `stat.UF_NOUNLINK`:
684 | file may not be renamed or deleted
685 | real value is 0x00000010 (16)
686 | `stat.UF_COMPRESSED`:
687 | OS X: file is hfs-compressed
688 | real value is 0x00000020 (32)
689 | `stat.UF_HIDDEN`:
690 | OS X: file should not be displayed
691 | real value is 0x00008000 (32768)
692 | `stat.SF_ARCHIVED`:
693 | file may be archived
694 | real value is 0x00010000 (65536)
695 | `stat.SF_IMMUTABLE`:
696 | file may not be changed
697 | real value is 0x00020000 (131072)
698 | `stat.SF_APPEND`:
699 | file may only be appended to
700 | real value is 0x00040000 (262144)
701 | `stat.SF_NOUNLINK`:
702 | file may not be renamed or deleted
703 | real value is 0x00100000 (1048576)
704 | `stat.SF_SNAPSHOT`:
705 | file is a snapshot file
706 | real value is 0x00200000 (2097152)
707 |
708 | If the optional initialization parameter `self.follow_symlinks` is
709 | specified as False, and the last element of the path is a symbolic
710 | link, the action will point to the symbolic link itself, not to the
711 | path to which the link points.
712 |
713 | Warning, do not attempt to set flags for important files and
714 | directories in your system, this may cause your system failure,
715 | unable to start!
716 |
717 | This method may not be available on your platform, using them will
718 | raise `NotImplementedError` if unavailable.
719 | """
720 |
721 | def lchflags(self, flags: int) -> None:
722 | """Set the flag for the file or directory, like `self.chflags`, but
723 | do not follow symbolic links."""
724 | self.__class__(self.name, follow_symlinks=False).chflags(flags)
725 |
726 | def chattr(self, operator: Literal['+', '-', '='], attrs: str) -> None:
727 | """
728 | Change the hidden attributes of the file or directory, call the Unix
729 | system command `chattr` internally.
730 |
731 | @param operator
732 | Specify an operator "+", "-", or "=". Used with the parameter
733 | `attributes` to add, remove, or reset certain attributes.
734 |
735 | @param attrs
736 | a: Only data can be appended.
737 | A: Tell the system not to change the last access time to the
738 | file or directory. However, this attribute is automatically
739 | removed after manual modification.
740 | c: Compress the file or directory and save it.
741 | d: Exclude the file or directory from the "dump" operation, the
742 | file or directory is not backed up by "dump" when the "dump"
743 | program is executed.
744 | e: Default attribute, this attribute indicates that the file or
745 | directory is using an extended partition to map blocks on
746 | disk.
747 | D: Check for errors in the compressed file.
748 | i: The file or directory is not allowed to be modified.
749 | u: Prevention of accidental deletion, when the file or directory
750 | is deleted, the system retains the data block so that it can
751 | recover later.
752 | s: As opposed to the attribute "u", when deleting the file or
753 | directory, it will be completely deleted (fill the disk
754 | partition with 0) and cannot be restored.
755 | S: Update the file or directory instantly.
756 | t: The tail-merging, file system support tail merging.
757 | ...
758 | More attributes that are rarely used (or no longer used), you
759 | can refer to the manual of the Unix system command `chattr`.
760 |
761 | Use Warning, the implementation of method `chattr` is to directly
762 | call the system command `chattr`, so this is very unreliable. Also,
763 | do not attempt to modify hidden attributes of important files and
764 | directories in your system, this may cause your system failure,
765 | unable to start!
766 | """
767 |
768 | def lsattr(self) -> str:
769 | """
770 | Get the hidden attributes of the file or directory, call the Unix
771 | system command `lsattr` internally.
772 |
773 | Use Warning, the implementation of method `lsattr` is to directly
774 | call the system command `lsattr`, so this is very unreliable.
775 | """
776 |
777 | def exattr(self, attr: str, /) -> bool:
778 | """
779 | Check whether the file or directory has the hidden attribute and
780 | return True or False.
781 |
782 | The usage of parameter `attr` can be seen in method `self.chattr`.
783 | """
784 | return attr in self.lsattr()
785 |
786 | if sys.platform == 'linux':
787 | def getxattr(self, attribute: BytesOrStr, /) -> bytes:
788 | """Return the value of extended attribute on path, you can look
789 | up `os.getxattr` for more description."""
790 |
791 | def setxattr(
792 | self,
793 | attribute: BytesOrStr,
794 | value: bytes,
795 | *,
796 | flags: Optional[int] = None
797 | ) -> None:
798 | """
799 | Set extended attribute on path to value, you can look up
800 | `os.setxattr` for more description.
801 |
802 | If the optional initialization parameter `self.follow_symlinks`
803 | is specified as False, and the last element of the path is a
804 | symbolic link, the action will point to the symbolic link
805 | itself, not to the path to which the link points.
806 | """
807 |
808 | def listxattr(self) -> List[str]:
809 | """
810 | Return a list of extended attributes on path, you can look up
811 | `os.listxattr` for more description.
812 |
813 | If the optional initialization parameter `self.follow_symlinks`
814 | is specified as False, and the last element of the path is a
815 | symbolic link, the action will point to the symbolic link
816 | itself, not to the path to which the link points.
817 | """
818 |
819 | def removexattr(self, attribute: BytesOrStr, /) -> None:
820 | """
821 | Remove extended attribute on path, you can look up
822 | `os.removexattr` for more description.
823 |
824 | If the optional initialization parameter `self.follow_symlinks`
825 | is specified as False, and the last element of the path is a
826 | symbolic link, the action will point to the symbolic link
827 | itself, not to the path to which the link points.
828 | """
829 |
830 | def utime(
831 | self,
832 | /,
833 | times: Optional[Tuple[Union[float, int], Union[float, int]]] = None
834 | ) -> None:
835 | """
836 | Set the access and modified time of the file or directory, call
837 | `os.utime` internally.
838 |
839 | @param times
840 | Pass in a tuple (atime, mtime) to specify access time and modify
841 | time respectively. If not specified then use current time.
842 |
843 | If the optional initialization parameter `self.follow_symlinks` is
844 | specified as False, and the last element of the path is a symbolic link,
845 | the action will point to the symbolic link itself, not to the path to
846 | which the link points.
847 | """
848 |
849 |
850 | class Directory(Path):
851 | """Pass a directory path link to get a directory object, which you can then
852 | use to do anything a directory can do."""
853 |
854 | def __getitem__(self, name: BytesOrStr) -> PathType:
855 | path: PathLink = os.path.join(self, name)
856 |
857 | if self.strict:
858 | if os.path.isdir(path):
859 | return Directory(path, strict=self.strict)
860 | if os.path.isfile(path):
861 | return File(path)
862 | if os.path.exists(name):
863 | return Path(name)
864 | raise SystemPathNotFoundError
865 |
866 | return SystemPath(path)
867 |
868 | def __delitem__(self, name: BytesOrStr) -> None:
869 | Path(os.path.join(self, name)).delete()
870 |
871 | def __iter__(self) -> Iterator[Union['Directory', 'File', Path]]:
872 | return self.subpaths
873 |
874 | def __bool__(self) -> bool:
875 | return self.isdir
876 |
877 | @staticmethod
878 | def home(
879 | *,
880 | strict: Optional[bool] = None,
881 | follow_symlinks: Optional[bool] = None
882 | ) -> 'Directory':
883 | return Directory(
884 | '~', strict=strict, follow_symlinks=follow_symlinks
885 | ).expanduser()
886 |
887 | @property
888 | def subpaths(self) -> Iterator[Union['Directory', 'File', Path]]:
889 | """Get the instances of `Directory` or `File` for all subpaths (single
890 | layer) in the directory."""
891 |
892 | @property
893 | def subpath_names(self) -> List[BytesOrStr]:
894 | """Get the names of all subpaths (single layer) in the directory. Call
895 | `os.listdir` internally."""
896 |
897 | def scandir(self) -> Iterator[os.DirEntry]:
898 | """Get instances of `os.DirEntry` for all files and subdirectories
899 | (single layer) in the directory, call `os.scandir` internally."""
900 |
901 | def tree(
902 | self,
903 | *,
904 | level: Optional[int] = None,
905 | downtop: Optional[bool] = None,
906 | omit_dir: Optional[bool] = None,
907 | pure_path: Optional[bool] = None,
908 | shortpath: Optional[bool] = None
909 | ) -> Iterator[Union[Path, PathLink]]:
910 | return tree(
911 | self.name,
912 | level =level,
913 | downtop =downtop,
914 | omit_dir =omit_dir,
915 | pure_path=pure_path,
916 | shortpath=shortpath
917 | )
918 |
919 | def walk(
920 | self,
921 | *,
922 | topdown: Optional[bool] = None,
923 | onerror: Optional[Callable] = None
924 | ) -> Iterator[Tuple[PathLink, List[BytesOrStr], List[BytesOrStr]]]:
925 | """
926 | Directory tree generator, recurse the directory to get all
927 | subdirectories and files, yield a 3-tuple for each subdirectory, call
928 | `os.walk` internally.
929 |
930 | The yielding 3-tuple is as follows:
931 | (current_directory_path, all_subdirectory_names, all_file_names)
932 |
933 | @param topdown
934 | The default is True, generate the directory tree from the outside
935 | in. If specified as False, from the inside out.
936 |
937 | @param onerror
938 | An optional error handler, for more instructions see `os.walk`.
939 | """
940 |
941 | def search(
942 | self,
943 | slicing: BytesOrStr,
944 | /, *,
945 | level: Optional[int] = None,
946 | omit_dir: Optional[bool] = None,
947 | pure_path: Optional[bool] = None,
948 | shortpath: Optional[bool] = None
949 | ) -> Iterator[Union[PathType, PathLink]]:
950 | """
951 | Search for all paths containing the specified string fragment in the
952 | current directory (and its subdirectories, according to the specified
953 | search depth). It traverses the directory tree, checking whether each
954 | path (which can be the path of a file or subdirectory) contains the
955 | specified slicing string `slicing`. If a matching path is found, it
956 | produces these paths as results.
957 |
958 | @param slicing
959 | The path slicing, which can be any part of the path.
960 |
961 | @param level
962 | Recursion depth of the directory, default is deepest. An int must be
963 | passed in, any integer less than 1 is considered to be 1, warning
964 | passing decimals can cause depth confusion.
965 |
966 | @param omit_dir
967 | Omit all subdirectories when yielding paths. The default is False.
968 |
969 | @param pure_path
970 | By default, if the subpath is a directory then yield a `Directory`
971 | object, if the subpath is a file then yield a `File` object. If set
972 | this parameter to True, directly yield the path link string (or
973 | bytes). This parameter is not recommended for use.
974 |
975 | @param shortpath
976 | Yield short path link string, delete the `dirpath` from the left end
977 | of the path, used with the parameter `pure_path`. The default is
978 | False.
979 | """
980 |
981 | def copytree(
982 | self,
983 | dst: Union['Directory', PathLink],
984 | /, *,
985 | symlinks: Optional[bool] = None,
986 | ignore: Optional[CopyTreeIgnore] = None,
987 | copy_function: Optional[CopyFunction] = None,
988 | ignore_dangling_symlinks: Optional[bool] = None,
989 | dirs_exist_ok: Optional[bool] = None
990 | ) -> Union['Directory', PathLink]:
991 | """
992 | Copy the directory tree recursively, call `shutil.copytree` internally.
993 |
994 | @param dst
995 | Where to copy the directory, hopefully pass in an instance of
996 | `Directory`, can also pass in a file path link.
997 |
998 | @param symlinks
999 | For symbolic links in the source tree, the content of the file to
1000 | which the symbolic link points is copied by default. If this
1001 | parameter is set to True, the symbolic link itself is copied.
1002 | The default is False.
1003 |
1004 | If the file to which the symbolic link points does not exist, raise
1005 | an exception at the end of the replication process. If you do not
1006 | want this exception raised, set the parameter
1007 | `ignore_dangling_symlinks` to True.
1008 |
1009 | @param ignore
1010 | An optional callable parameter, used to manipulate the directory
1011 | `copytree` is accessing, and return a list of content names relative
1012 | to those that should not be copied. Can like this:
1013 |
1014 | >>> def func(
1015 | >>> src: PathLink, names: List[BytesOrStr]
1016 | >>> ) -> List[BytesOrStr]:
1017 | >>> '''
1018 | >>> @param src
1019 | >>> The directory that `copytree` is accessing.
1020 | >>> @param names
1021 | >>> A list of the content names of the directories being
1022 | >>> accessed.
1023 | >>> @return
1024 | >>> A list of content names relative to those that
1025 | >>> should not be copied
1026 | >>> '''
1027 | >>> return [b'alpha.txt', b'beta.txt']
1028 |
1029 | For more instructions see `shutil.copytree`.
1030 |
1031 | @param copy_function
1032 | The optional parameter `copy_function` will be passed directly to
1033 | `shutil.copytree` and default value is `shutil.copy2`.
1034 |
1035 | @param ignore_dangling_symlinks
1036 | Used to ignore exceptions raised by symbolic link errors, use with
1037 | parameter `symlinks`. The default is False. This parameter has no
1038 | effect on platforms that do not support `os.symlink`.
1039 |
1040 | @param dirs_exist_ok
1041 | If the destination path already exists will raise `FileExistsError`,
1042 | can set this parameter to True to silence the exception and
1043 | overwrite the files in the target. Default is False.
1044 |
1045 | @return: The parameter `dst` is passed in, without any modification.
1046 | """
1047 |
1048 | def clear(self) -> None:
1049 | """
1050 | Clear the directory.
1051 |
1052 | Traverse everything in the directory and delete it, call `self.rmtree`
1053 | for the directories and `File.remove` for the files (or anything else).
1054 | """
1055 |
1056 | def mkdir(
1057 | self,
1058 | mode: Optional[int] = None,
1059 | *,
1060 | ignore_exists: Optional[bool] = None
1061 | ) -> None:
1062 | """
1063 | Create the directory on your system, call `os.mkdir` internally.
1064 |
1065 | @param mode
1066 | Specify the access permissions for the directory, can be a
1067 | permission mask (0o600), can be a combination (0o600|stat.S_IFREG),
1068 | can be a bitwise (33152). Default is 0o777.
1069 |
1070 | This parameter is ignored if your platform is Windows.
1071 |
1072 | @param ignore_exists
1073 | If the directory already exists, call this method will raise
1074 | `FileExistsError`. But, if this parameter is set to True then
1075 | silently skip. The default is False.
1076 | """
1077 |
1078 | def makedirs(
1079 | self,
1080 | mode: Optional[int] = None,
1081 | *,
1082 | exist_ok: Optional[bool] = None
1083 | ) -> None:
1084 | """
1085 | Create the directory and all intermediate ones, super version of
1086 | `self.mkdir`. Call `os.makedirs` internally.
1087 |
1088 | @param mode
1089 | Specify the access permissions for the directory, can be a
1090 | permission mask (0o600), can be a combination (0o600|stat.S_IFREG),
1091 | can be a bitwise (33152). Default is 0o777.
1092 |
1093 | This parameter is ignored if your platform is Windows.
1094 |
1095 | @param exist_ok
1096 | If the directory already exists will raise `FileExistsError`, can
1097 | set this parameter to True to silence the exception. The default is
1098 | False.
1099 | """
1100 |
1101 | def rmdir(self) -> None:
1102 | """Delete the directory on your system, if the directory is not empty
1103 | then raise `OSError`. Call `os.rmdir` internally."""
1104 |
1105 | def removedirs(self) -> None:
1106 | """
1107 | Delete the directory and all empty intermediate ones, super version of
1108 | `self.rmdir`. Call `os.removedirs` internally.
1109 |
1110 | Delete from the far right, terminates until the whole path is consumed
1111 | or a non-empty directory is found. if the leaf directory is not empty
1112 | then raise `OSError`.
1113 | """
1114 |
1115 | def rmtree(
1116 | self,
1117 | *,
1118 | ignore_errors: Optional[bool] = None,
1119 | onerror: Optional[Callable] = None
1120 | ) -> None:
1121 | """
1122 | Delete the directory tree recursively, call `shutil.rmtree` internally.
1123 |
1124 | @param ignore_errors
1125 | If the directory does not exist will raise `FileNotFoundError`, can
1126 | set this parameter to True to silence the exception. The default is
1127 | False.
1128 |
1129 | @param onerror
1130 | An optional error handler, described more see `shutil.rmtree`.
1131 | """
1132 |
1133 | def chdir(self) -> None:
1134 | """Change the working directory of the current process to the directory.
1135 | """
1136 | os.chdir(self)
1137 |
1138 |
1139 | class File(Path):
1140 | """Pass a file path link to get a file object, which you can then use to do
1141 | anything a file can do."""
1142 |
1143 | def __bool__(self) -> bool:
1144 | return self.isfile
1145 |
1146 | def __contains__(self, subcontent: bytes, /) -> bool:
1147 | return subcontent in self.contents
1148 |
1149 | def __iter__(self) -> Iterator[bytes]:
1150 | yield from self.contents
1151 |
1152 | @property
1153 | def open(self) -> 'Open':
1154 | return Open(self)
1155 |
1156 | @property
1157 | def ini(self) -> 'INI':
1158 | return INI(self)
1159 |
1160 | @property
1161 | def csv(self) -> 'CSV':
1162 | return CSV(self)
1163 |
1164 | @property
1165 | def json(self) -> 'JSON':
1166 | return JSON(self)
1167 |
1168 | @property
1169 | def yaml(self) -> 'YAML':
1170 | return YAML(self)
1171 |
1172 | content = property(
1173 | lambda self : self.contents.read(),
1174 | lambda self, content: self.contents.overwrite(content),
1175 | lambda self : self.contents.clear(),
1176 | """Quickly read, rewrite, or empty all contents of the file (in binary
1177 | mode). Note that for other operations on the file contents, use
1178 | `contents`."""
1179 | )
1180 |
1181 | @property
1182 | def contents(self) -> 'Content':
1183 | """Operation the file content, super version of `self.content`."""
1184 | return Content(self)
1185 |
1186 | @contents.setter
1187 | def contents(self, content: ['Content', bytes]) -> None:
1188 | """Do nothing, syntax hints for compatibility with `Content.__iadd__`
1189 | and `Content.__ior__` only."""
1190 |
1191 | def splitext(self) -> Tuple[BytesOrStr, BytesOrStr]:
1192 | return os.path.splitext(self)
1193 |
1194 | @property
1195 | def extension(self) -> BytesOrStr:
1196 | return os.path.splitext(self)[1]
1197 |
1198 | def copy(self, dst: Union['File', PathLink], /) -> Union['File', PathLink]:
1199 | """
1200 | Copy the file to another location, call `shutil.copyfile` internally.
1201 |
1202 | @param dst:
1203 | Where to copy the file, hopefully pass in an instance of `File`, can
1204 | also pass in a file path link.
1205 |
1206 | If the optional initialization parameter `self.follow_symlinks` is
1207 | specified as False, and the last element of the file path is a symbolic
1208 | link, will create a new symbolic link instead of copy the file to which
1209 | the link points to.
1210 |
1211 | @return: The parameter `dst` is passed in, without any modification.
1212 | """
1213 |
1214 | def copycontent(
1215 | self,
1216 | dst: Union['File', 'SupportsWrite[bytes]'],
1217 | /, *,
1218 | bufsize: Optional[int] = None
1219 | ) -> Union['File', 'SupportsWrite[bytes]']:
1220 | """
1221 | Copy the file contents to another file.
1222 |
1223 | @param dst
1224 | Where to copy the file contents, hopefully pass in an instance of
1225 | `File`. Can also pass in a stream of the destination file (or called
1226 | handle), it must have at least writable or append permission.
1227 |
1228 | @param bufsize
1229 | The buffer size, the length of each copy, default is 64K (if your
1230 | platform is Windows then 1M). Passing -1 turns off buffering.
1231 |
1232 | @return: The parameter `dst` is passed in, without any modification.
1233 | """
1234 | warnings.warn(
1235 | f'will be deprecated soon, replaced to {self.contents.copy}.',
1236 | DeprecationWarning
1237 | )
1238 |
1239 | def link(self, dst: Union['File', PathLink], /) -> Union['File', PathLink]:
1240 | """
1241 | Create a hard link to the file, call `os.link` internally.
1242 |
1243 | @param dst:
1244 | Where to create the hard link for the file, hopefully pass in an
1245 | instance of `File`, can also pass in a file path link.
1246 |
1247 | The optional initialization parameter `self.dir_fd` will be passed to
1248 | parameters `src_dir_fd` and `dst_dir_fd` of `os.link`.
1249 |
1250 | If the optional initialization parameter `self.follow_symlinks` is
1251 | specified as False, and the last element of the file path is a symbolic
1252 | link, will create a link to the symbolic link itself instead of the file
1253 | to which the link points to.
1254 |
1255 | @return: The parameter `dst` is passed in, without any modification.
1256 | """
1257 |
1258 | def mknod(
1259 | self,
1260 | mode: Optional[int] = None,
1261 | *,
1262 | device: Optional[int] = None,
1263 | ignore_exists: Optional[bool] = None
1264 | ) -> None:
1265 | """
1266 | Create the file, call `os.mknod` internally, but if your platform is
1267 | Windows then internally call `open(self.name, 'x')`.
1268 |
1269 | @param mode
1270 | Specify the access permissions of the file, can be a permission
1271 | mask (0o600), can be a combination (0o600|stat.S_IFREG), can be a
1272 | bitwise (33152), and default is 0o600(-rw-------).
1273 |
1274 | @param device
1275 | Default 0, this parameter may not be available on your platform,
1276 | using them will ignore if unavailable. You can look up `os.mknod`
1277 | for more description.
1278 |
1279 | @param ignore_exists
1280 | If the file already exists, call this method will raise
1281 | `FileExistsError`. But, if this parameter is set to True then
1282 | silently skip. The default is False.
1283 | """
1284 |
1285 | def mknods(
1286 | self,
1287 | mode: Optional[int] = None,
1288 | *,
1289 | device: Optional[int] = None,
1290 | ignore_exists: Optional[bool] = None
1291 | ) -> None:
1292 | """Create the file and all intermediate paths, super version of
1293 | `self.mknod`."""
1294 | self.dirname.makedirs(mode, exist_ok=True)
1295 | self.mknod(mode, device=device, ignore_exists=ignore_exists)
1296 |
1297 | def create(
1298 | self,
1299 | mode: Optional[int] = None,
1300 | *,
1301 | device: Optional[int] = None,
1302 | ignore_exists: Optional[bool] = None
1303 | ):
1304 | warnings.warn(
1305 | f'will be deprecated soon, replaced to {self.mknod}.',
1306 | DeprecationWarning
1307 | )
1308 | self.mknod(mode, device=device, ignore_exists=ignore_exists)
1309 |
1310 | def creates(
1311 | self,
1312 | mode: Optional[int] = None,
1313 | *,
1314 | device: Optional[int] = None,
1315 | ignore_exists: Optional[bool] = None
1316 | ) -> None:
1317 | warnings.warn(
1318 | f'will be deprecated soon, replaced to {self.mknods}.',
1319 | DeprecationWarning
1320 | )
1321 | self.mknods(mode, device=device, ignore_exists=ignore_exists)
1322 |
1323 | def remove(self, *, ignore_errors: Optional[bool] = None) -> None:
1324 | """
1325 | Remove the file, call `os.remove` internally.
1326 |
1327 | @param ignore_errors
1328 | If the file does not exist will raise `FileNotFoundError`, can set
1329 | this parameter to True to silence the exception. The default is
1330 | False.
1331 | """
1332 |
1333 | def unlink(self) -> None:
1334 | """Remove the file, like `self.remove`, call `os.unlink` internally."""
1335 |
1336 | def contains(self, subcontent: bytes, /) -> bool:
1337 | return self.contents.contains(subcontent)
1338 |
1339 | def truncate(self, length: int) -> None:
1340 | self.contents.truncate(length)
1341 |
1342 | def clear(self) -> None:
1343 | self.contents.clear()
1344 |
1345 | def md5(self, salting: Optional[bytes] = None) -> str:
1346 | return self.contents.md5(salting)
1347 |
1348 | def read(
1349 | self,
1350 | size: Optional[int] = None,
1351 | /, *,
1352 | encoding: Optional[str] = None
1353 | ) -> str:
1354 | warnings.warn(
1355 | f'deprecated, replaced to {self.content} or {self.contents.read}.',
1356 | DeprecationWarning
1357 | )
1358 | return self.open.r(encoding=encoding).read(size)
1359 |
1360 | def write(self, content: str, /, *, encoding: Optional[str] = None) -> int:
1361 | warnings.warn(
1362 | f'deprecated, replaced to {self.content} or {self.contents.write}.',
1363 | DeprecationWarning
1364 | )
1365 | return self.open.w(encoding=encoding).write(content)
1366 |
1367 | def append(self, content: str, /, *, encoding: Optional[str] = None) -> int:
1368 | warnings.warn(
1369 | f'deprecated, replaced to {self.contents.append}.',
1370 | DeprecationWarning
1371 | )
1372 | return self.open.a(encoding=encoding).write(content)
1373 |
1374 |
1375 | class Open:
1376 | """
1377 | Open a file and return a file stream (or called handle).
1378 |
1379 | >>> f: BinaryIO = Open('alpha.bin').rb() # open for reading in binary mode.
1380 | >>> f: TextIO = Open('alpha.txt').r() # open for reading in text mode.
1381 |
1382 | Pass in an instance of `File` (or a file path link) at instantiation time.
1383 | At instantiation time (do nothing) the file will not be opened, only when
1384 | you call one of the following methods, the file will be opened (call once,
1385 | open once), open mode equals method name (where method `rb_plus` equals mode
1386 | "rb+").
1387 |
1388 | ============================== IN BINARY MODE ==============================
1389 | | Method | Description |
1390 | ----------------------------------------------------------------------------
1391 | | rb | open to read, if the file does not exist then raise |
1392 | | | `FileNotFoundError` |
1393 | ----------------------------------------------------------------------------
1394 | | wb | open to write, truncate the file first, if the file does not |
1395 | | | exist then create it |
1396 | ----------------------------------------------------------------------------
1397 | | xb | create a new file and open it to write, if the file already |
1398 | | | exists then raise `FileExistsError` |
1399 | ----------------------------------------------------------------------------
1400 | | ab | open to write, append to the end of the file, if the file does |
1401 | | | not exist then create it |
1402 | ----------------------------------------------------------------------------
1403 | | rb_plus | open to read and write, if the file does not exist then raise |
1404 | | | `FileNotFoundError` |
1405 | ----------------------------------------------------------------------------
1406 | | wb_plus | open to write and read, truncate the file first, if the file |
1407 | | | does not exist then create it |
1408 | ----------------------------------------------------------------------------
1409 | | xb_plus | create a new file and open it to write and read, if the file |
1410 | | | already exists then raise `FileExistsError` |
1411 | ----------------------------------------------------------------------------
1412 | | ab_plus | open to write and read, append to the end of the file, if the |
1413 | | | file does not exist then create it |
1414 | ----------------------------------------------------------------------------
1415 |
1416 | =============================== IN TEXT MODE ===============================
1417 | | Method | Description |
1418 | ----------------------------------------------------------------------------
1419 | | r | open to read, if the file does not exist then raise |
1420 | | | `FileNotFoundError` |
1421 | ----------------------------------------------------------------------------
1422 | | w | open to write, truncate the file first, if the file does not |
1423 | | | exist then create it |
1424 | ----------------------------------------------------------------------------
1425 | | x | create a new file and open it to write, if the file already |
1426 | | | exists then raise `FileExistsError` |
1427 | ----------------------------------------------------------------------------
1428 | | a | open to write, append to the end of the file, if the file does |
1429 | | | not exist then create it |
1430 | ----------------------------------------------------------------------------
1431 | | r_plus | open to read and write, if the file does not exist then raise |
1432 | | | `FileNotFoundError` |
1433 | ----------------------------------------------------------------------------
1434 | | w_plus | open to write and read, truncate the file first, if the file |
1435 | | | does not exist then create it |
1436 | ----------------------------------------------------------------------------
1437 | | x_plus | create a new file and open it to write and read, if the file |
1438 | | | already exists then raise `FileExistsError` |
1439 | ----------------------------------------------------------------------------
1440 | | a_plus | open to write and read, append to the end of the file, if the |
1441 | | | file does not exist then create it |
1442 | ----------------------------------------------------------------------------
1443 |
1444 | @param bufsize
1445 | Pass an integer greater than 0 to set the buffer size, 0 in bianry mode
1446 | to turn off buffering, 1 in text mode to use line buffering. The buffer
1447 | size is selected by default using a heuristic (reference
1448 | `io.DEFAULT_BUFFER_SIZE`, on most OS, the buffer size is usually 8192 or
1449 | 4096 bytes), for "interactive" text files (files for which call
1450 | `isatty()` returns True), line buffering is used by default.
1451 |
1452 | @param encoding
1453 | The name of the encoding used to decode or encode the file (usually
1454 | specified as "UTF-8"). The default encoding is platform-based and
1455 | `locale.getpreferredencoding(False)` is called to get the current locale
1456 | encoding. For the list of supported encodings, see the `codecs` module.
1457 |
1458 | @param errors
1459 | Specify how to handle encoding errors (how strict encoding is), default
1460 | is "strict" (maximum strictness, equivalent to passing None), if
1461 | encoding error then raise `ValueError`. You can pass "ignore" to ignore
1462 | encoding errors (caution ignoring encoding errors may result in data
1463 | loss). The supported encoding error handling modes are as follows:
1464 |
1465 | --------------------------------------------------------------------
1466 | | static | raise `ValueError` (or its subclass) |
1467 | --------------------------------------------------------------------
1468 | | ignore | ignore the character, continue with the next |
1469 | --------------------------------------------------------------------
1470 | | replace | replace with a suitable character |
1471 | --------------------------------------------------------------------
1472 | | surrogateescape | replace with private code points U+DCnn |
1473 | --------------------------------------------------------------------
1474 | | xmlcharrefreplace | replace with a suitable XML character |
1475 | | | reference (only for encoding) |
1476 | --------------------------------------------------------------------
1477 | | backslashreplace | replace with backslash escape sequence |
1478 | --------------------------------------------------------------------
1479 | | namereplace | replace \\N{...} escape sequence (only for |
1480 | | | encoding) |
1481 | --------------------------------------------------------------------
1482 |
1483 | The allowed set of values can be extended via `codecs.register_error`,
1484 | for more instructions see the `codecs.Codec` class.
1485 |
1486 | @param newline
1487 | Specify how universal newline character works, can be None, "", "\n",
1488 | "\r" and "\r\n". For more instructions see the `_io.TextIOWrapper`
1489 | class.
1490 |
1491 | @param line_buffering
1492 | If set to True, automatically call `flush()` when writing contains a
1493 | newline character, The default is False.
1494 |
1495 | @param write_through
1496 | We do not find any description of this parameter in the Python3 source
1497 | code.
1498 |
1499 | @param opener
1500 | You can customize the file opener by this parameter, custom file opener
1501 | can like this:
1502 | >>> def opener(file: str, flags: int) -> int:
1503 | >>> return os.open(file, os.O_RDONLY)
1504 | """
1505 |
1506 | def __init__(self, file: Union[File, PathLink], /):
1507 | self.file = file
1508 |
1509 | def rb(
1510 | self,
1511 | *,
1512 | bufsize: Optional[int] = None,
1513 | opener: Optional[FileOpener] = None
1514 | ) -> BinaryIO: ...
1515 |
1516 | def wb(
1517 | self,
1518 | *,
1519 | bufsize: Optional[int] = None,
1520 | opener: Optional[FileOpener] = None
1521 | ) -> BinaryIO: ...
1522 |
1523 | def xb(
1524 | self,
1525 | *,
1526 | bufsize: Optional[int] = None,
1527 | opener: Optional[FileOpener] = None
1528 | ) -> BinaryIO: ...
1529 |
1530 | def ab(
1531 | self,
1532 | *,
1533 | bufsize: Optional[int] = None,
1534 | opener: Optional[FileOpener] = None
1535 | ) -> BinaryIO: ...
1536 |
1537 | def rb_plus(
1538 | self,
1539 | *,
1540 | bufsize: Optional[int] = None,
1541 | opener: Optional[FileOpener] = None
1542 | ) -> BinaryIO: ...
1543 |
1544 | def wb_plus(
1545 | self,
1546 | *,
1547 | bufsize: Optional[int] = None,
1548 | opener: Optional[FileOpener] = None
1549 | ) -> BinaryIO: ...
1550 |
1551 | def xb_plus(
1552 | self,
1553 | *,
1554 | bufsize: Optional[int] = None,
1555 | opener: Optional[FileOpener] = None
1556 | ) -> BinaryIO: ...
1557 |
1558 | def ab_plus(
1559 | self,
1560 | *,
1561 | bufsize: Optional[int] = None,
1562 | opener: Optional[FileOpener] = None
1563 | ) -> BinaryIO: ...
1564 |
1565 | def r(
1566 | self,
1567 | *,
1568 | bufsize: Optional[int] = None,
1569 | encoding: Optional[str] = None,
1570 | errors: Optional[str] = None,
1571 | newline: Optional[FileNewline] = None,
1572 | opener: Optional[FileOpener] = None
1573 | ) -> TextIO: ...
1574 |
1575 | def w(
1576 | self,
1577 | *,
1578 | bufsize: Optional[int] = None,
1579 | encoding: Optional[str] = None,
1580 | errors: Optional[str] = None,
1581 | newline: Optional[FileNewline] = None,
1582 | line_buffering: Optional[bool] = None,
1583 | write_through: Optional[bool] = None,
1584 | opener: Optional[FileOpener] = None
1585 | ) -> TextIO: ...
1586 |
1587 | def x(
1588 | self,
1589 | *,
1590 | bufsize: Optional[int] = None,
1591 | encoding: Optional[str] = None,
1592 | errors: Optional[str] = None,
1593 | newline: Optional[FileNewline] = None,
1594 | line_buffering: Optional[bool] = None,
1595 | write_through: Optional[bool] = None,
1596 | opener: Optional[FileOpener] = None
1597 | ) -> TextIO: ...
1598 |
1599 | def a(
1600 | self,
1601 | *,
1602 | bufsize: Optional[int] = None,
1603 | encoding: Optional[str] = None,
1604 | errors: Optional[str] = None,
1605 | newline: Optional[FileNewline] = None,
1606 | line_buffering: Optional[bool] = None,
1607 | write_through: Optional[bool] = None,
1608 | opener: Optional[FileOpener] = None
1609 | ) -> TextIO: ...
1610 |
1611 | def r_plus(
1612 | self,
1613 | *,
1614 | bufsize: Optional[int] = None,
1615 | encoding: Optional[str] = None,
1616 | errors: Optional[str] = None,
1617 | newline: Optional[FileNewline] = None,
1618 | line_buffering: Optional[bool] = None,
1619 | write_through: Optional[bool] = None,
1620 | opener: Optional[FileOpener] = None
1621 | ) -> TextIO: ...
1622 |
1623 | def w_plus(
1624 | self,
1625 | *,
1626 | bufsize: Optional[int] = None,
1627 | encoding: Optional[str] = None,
1628 | errors: Optional[str] = None,
1629 | newline: Optional[FileNewline] = None,
1630 | line_buffering: Optional[bool] = None,
1631 | write_through: Optional[bool] = None,
1632 | opener: Optional[FileOpener] = None
1633 | ) -> TextIO: ...
1634 |
1635 | def x_plus(
1636 | self,
1637 | *,
1638 | bufsize: Optional[int] = None,
1639 | encoding: Optional[str] = None,
1640 | errors: Optional[str] = None,
1641 | newline: Optional[FileNewline] = None,
1642 | line_buffering: Optional[bool] = None,
1643 | write_through: Optional[bool] = None,
1644 | opener: Optional[FileOpener] = None
1645 | ) -> TextIO: ...
1646 |
1647 | def a_plus(
1648 | self,
1649 | *,
1650 | bufsize: Optional[int] = None,
1651 | encoding: Optional[str] = None,
1652 | errors: Optional[str] = None,
1653 | newline: Optional[FileNewline] = None,
1654 | line_buffering: Optional[bool] = None,
1655 | write_through: Optional[bool] = None,
1656 | opener: Optional[FileOpener] = None
1657 | ) -> TextIO: ...
1658 |
1659 |
1660 | class Content:
1661 | """Pass in an instance of `File` (or a file path link) to get a file content
1662 | object, which you can then use to operation the contents of the file (in
1663 | binary mode)."""
1664 |
1665 | def __init__(self, file: Union[File, PathLink], /):
1666 | self.file = file
1667 |
1668 | def __bytes__(self) -> bytes:
1669 | return self.read()
1670 |
1671 | def __ior__(self, other: Union['Content', bytes], /) -> Self:
1672 | self.write(other)
1673 | return self
1674 |
1675 | def __iadd__(self, other: Union['Content', bytes], /) -> Self:
1676 | self.append(other)
1677 | return self
1678 |
1679 | def __eq__(self, other: Union['Content', bytes], /) -> bool:
1680 | """Whether the contents of the current file equal the contents of
1681 | another file (or a bytes object). If they all point to the same file
1682 | then direct return True."""
1683 |
1684 | def __contains__(self, subcontent: bytes, /) -> bool:
1685 | return self.contains(subcontent)
1686 |
1687 | def __iter__(self) -> Iterator[bytes]:
1688 | """Iterate over the file by line, omitting newline symbol and ignoring
1689 | the last blank line."""
1690 |
1691 | def __len__(self) -> int:
1692 | """Return the length (actually the size) of the file contents."""
1693 |
1694 | def __bool__(self) -> bool:
1695 | """Return True if the file has content else False."""
1696 |
1697 | def read(self, size: Optional[int] = None, /) -> bytes:
1698 | return Open(self.file).rb().read(size)
1699 |
1700 | def write(self, content: Union['Content', bytes], /) -> int:
1701 | """Overwrite the current file content from another file content (or a
1702 | bytes object)."""
1703 |
1704 | def overwrite(self, content: Union['Content', bytes], /) -> int:
1705 | warnings.warn(
1706 | f'will be deprecated soon, replaced to {self.write}.',
1707 | DeprecationWarning
1708 | )
1709 | return self.write(content)
1710 |
1711 | def append(self, content: Union['Content', bytes], /) -> int:
1712 | """Append the another file contents (or a bytes object) to the current
1713 | file."""
1714 |
1715 | def contains(self, subcontent: bytes, /) -> bool:
1716 | """Return True if the current file content contain `subcontent` else
1717 | False."""
1718 |
1719 | def copy(
1720 | self,
1721 | dst: Union['Content', 'SupportsWrite[bytes]'] = None,
1722 | /, *,
1723 | bufsize: Optional[int] = None
1724 | ) -> None:
1725 | """
1726 | Copy the file contents to another file.
1727 |
1728 | @param dst
1729 | Where to copy the file contents, hopefully pass in an instance of
1730 | `Content`. Can also pass in a stream of the destination file (or
1731 | called handle), it must have at least writable or append permission.
1732 |
1733 | @param bufsize
1734 | The buffer size, the length of each copy, default is 64K (if your
1735 | platform is Windows then 1M). Passing -1 turns off buffering.
1736 | """
1737 |
1738 | def truncate(self, length: int, /) -> None:
1739 | """Truncate the file content to specific length."""
1740 |
1741 | def clear(self) -> None:
1742 | self.truncate(0)
1743 |
1744 | def md5(self, salting: Optional[bytes] = None) -> str:
1745 | """Return the md5 string of the file content."""
1746 |
1747 |
1748 | def tree(
1749 | dirpath: Optional[PathLink] = None,
1750 | /, *,
1751 | level: Optional[int] = None,
1752 | downtop: Optional[bool] = None,
1753 | omit_dir: Optional[bool] = None,
1754 | pure_path: Optional[bool] = None,
1755 | shortpath: Optional[bool] = None
1756 | ) -> Iterator[Union[Path, PathLink]]:
1757 | """
1758 | Directory tree generator, recurse the directory to get all subdirectories
1759 | and files.
1760 |
1761 | @param dirpath
1762 | Specify a directory path link, recurse this directory on call to get all
1763 | subdirectories and files, default is current working directory (the
1764 | return value of `os.getcwd()`).
1765 |
1766 | @param level
1767 | Recursion depth of the directory, default is deepest. An int must be
1768 | passed in, any integer less than 1 is considered to be 1, warning
1769 | passing decimals can cause depth confusion.
1770 |
1771 | @param downtop
1772 | By default, the outer path is yielded first, from which the inner path
1773 | is yielded. If your requirements are opposite, set this parameter to
1774 | True.
1775 |
1776 | @param omit_dir
1777 | Omit all subdirectories when yielding paths. The default is False.
1778 |
1779 | @param pure_path
1780 | By default, if the subpath is a directory then yield a `Directory`
1781 | object, if the subpath is a file then yield a `File` object. If set this
1782 | parameter to True, directly yield the path link string (or bytes). This
1783 | parameter is not recommended for use.
1784 |
1785 | @param shortpath
1786 | Yield short path link string, delete the `dirpath` from the left end of
1787 | the path, used with the parameter `pure_path`. The default is False.
1788 | """
1789 |
1790 |
1791 | class INI:
1792 | """
1793 | Class to read and parse INI file.
1794 |
1795 | @param encoding
1796 | The encoding used to read files is usually specified as "UTF-8". The
1797 | default encoding is platform-based, and
1798 | `locale.getpreferredencoding(False)` is called to obtain the current
1799 | locale encoding. For a list of supported encodings, please refer to the
1800 | `codecs` module.
1801 |
1802 | @param defaults
1803 | A dictionary containing default key-value pairs to use if some options
1804 | are missing in the parsed configuration file.
1805 |
1806 | @param dict_type
1807 | The type used to represent the returned dictionary. The default is
1808 | `dict`, which means that sections and options in the configuration will
1809 | be preserved in the order they appear in the file.
1810 |
1811 | @param allow_no_value
1812 | A boolean specifying whether options without values are allowed. If set
1813 | to True, lines like `key=` will be accepted and the value of key will be
1814 | set to None.
1815 |
1816 | @param delimiters
1817 | A sequence of characters used to separate keys and values. The default
1818 | is `("=", ":")`, which means both "=" and ":" can be used as delimiters.
1819 |
1820 | @param comment_prefixes
1821 | A sequence of prefixes used to identify comment lines. The default is
1822 | `("#", ";")`, which means lines starting with "#" or ";" will be
1823 | considered comments.
1824 |
1825 | @param inline_comment_prefixes
1826 | A sequence of prefixes used to identify inline comments. The default is
1827 | None, which means inline comments are not supported.
1828 |
1829 | @param strict
1830 | A boolean specifying whether to parse strictly. If set to True, the
1831 | parser will report syntax errors, such as missing sections or incorrect
1832 | delimiters.
1833 |
1834 | @param empty_lines_in_values
1835 | A boolean specifying whether empty lines within values are allowed. If
1836 | set to True, values can span multiple lines, and empty lines will be
1837 | preserved.
1838 |
1839 | @param default_section
1840 | The name of the default section in the configuration file is "DEFAULT"
1841 | by default. If specified, any options that do not belong to any section
1842 | during parsing will be added to this default section.
1843 |
1844 | @param interpolation
1845 | Specifies the interpolation type. Interpolation is a substitution
1846 | mechanism that allows values in the configuration file to reference
1847 | other values. Supports `configparser.BasicInterpolation` and
1848 | `configparser.ExtendedInterpolation`.
1849 |
1850 | @param converters
1851 | A dictionary containing custom conversion functions used to convert
1852 | string values from the configuration file to other types. The keys are
1853 | the names of the conversion functions, and the values are the
1854 | corresponding conversion functions.
1855 | """
1856 |
1857 | def __init__(self, file: File, /):
1858 | self.file = file
1859 |
1860 | def read(
1861 | self,
1862 | *,
1863 | encoding: Optional[str] = None,
1864 | defaults: Optional[Mapping[str, str]] = None,
1865 | dict_type: Optional[Type[Mapping[str, str]]] = None,
1866 | allow_no_value: Optional[bool] = None,
1867 | delimiters: Optional[Sequence[str]] = None,
1868 | comment_prefixes: Optional[Sequence[str]] = None,
1869 | inline_comment_prefixes: Optional[Sequence[str]] = None,
1870 | strict: Optional[bool] = None,
1871 | empty_lines_in_values: Optional[bool] = None,
1872 | default_section: Optional[str] = None,
1873 | interpolation: Optional['Interpolation'] = None,
1874 | converters: Optional[ConvertersMap] = None
1875 | ) -> 'ConfigParser': ...
1876 |
1877 |
1878 | class CSV:
1879 | """
1880 | A class to handle CSV file reading and writing operations.
1881 |
1882 | @param dialect:
1883 | The dialect to use for the CSV file format. A dialect is a set of
1884 | specific parameters that define the format of a CSV file, such as the
1885 | delimiter, quote character, etc. "excel" is a commonly used default
1886 | dialect that uses a comma as the delimiter and a double quote as the
1887 | quote character.
1888 |
1889 | @param mode
1890 | The mode to open the file, only "w" or "a" are supported. The default is
1891 | "w".
1892 |
1893 | @param encoding
1894 | Specify the encoding for opening the file, usually specified as "UTF-8".
1895 | The default encoding is based on the platform, call
1896 | `locale.getpreferredencoding(False)` to get the current locale encoding.
1897 | See the `codecs` module for a list of supported encodings.
1898 |
1899 | @param delimiter:
1900 | The character used to separate fields. The default in the "excel"
1901 | dialect is a comma.
1902 |
1903 | @param quotechar:
1904 | The character used to quote fields. The default in the "excel" dialect
1905 | is a double quote.
1906 |
1907 | @param escapechar:
1908 | The character used to escape field content, default is None. If a field
1909 | contains the delimiter or quote character, the escape character can be
1910 | used to avoid ambiguity.
1911 |
1912 | @param doublequote:
1913 | If True (the default), quote characters in fields will be doubled. For
1914 | example, "Hello, World" will be written as \"""Hello, World\""".
1915 |
1916 | @param skipinitialspace:
1917 | If True, whitespace immediately following the delimiter is ignored. The
1918 | default is False.
1919 |
1920 | @param lineterminator:
1921 | The string used to terminate lines. The default is "\r\n", i.e.,
1922 | carriage return plus line feed.
1923 |
1924 | @param quoting:
1925 | Controls when quotes should be generated by the writer and recognized by
1926 | the reader. It can be any of the following values:
1927 | 0: Indicates that quotes should only be used when necessary (for
1928 | example, when the field contains the delimiter or quote
1929 | character);
1930 | 1: Indicates that quotes should always be used;
1931 | 2: Indicates that quotes should never be used;
1932 | 3: Indicates that double quotes should always be used.
1933 |
1934 | @param strict:
1935 | If True, raise errors for CSV format anomalies (such as extra quote
1936 | characters). The default is False, which does not raise errors.
1937 | """
1938 |
1939 | def __init__(self, file: File, /):
1940 | self.file = file
1941 |
1942 | def reader(
1943 | self,
1944 | dialect: Optional[CSVDialectLike] = None,
1945 | *,
1946 | encoding: Optional[str] = None,
1947 | delimiter: Optional[str] = None,
1948 | quotechar: Optional[str] = None,
1949 | escapechar: Optional[str] = None,
1950 | doublequote: Optional[bool] = None,
1951 | skipinitialspace: Optional[bool] = None,
1952 | lineterminator: Optional[str] = None,
1953 | quoting: Optional[int] = None,
1954 | strict: Optional[bool] = None
1955 | ) -> CSVReader: ...
1956 |
1957 | def writer(
1958 | self,
1959 | dialect: Optional[CSVDialectLike] = None,
1960 | *,
1961 | mode: Optional[Literal['w', 'a']] = None,
1962 | encoding: Optional[str] = None,
1963 | delimiter: Optional[str] = None,
1964 | quotechar: Optional[str] = None,
1965 | escapechar: Optional[str] = None,
1966 | doublequote: Optional[bool] = None,
1967 | skipinitialspace: Optional[bool] = None,
1968 | lineterminator: Optional[str] = None,
1969 | quoting: Optional[int] = None,
1970 | strict: Optional[bool] = None
1971 | ) -> CSVWriter: ...
1972 |
1973 |
1974 | class JSON:
1975 | """
1976 | A class for handling JSON operations with a file object. It provides
1977 | methods for loading JSON data from a file and dumping Python objects into a
1978 | file as JSON.
1979 |
1980 | @param cls
1981 | Pass a class used for decoding or encoding JSON data. By default,
1982 | `json.JSONDecoder` is used for decoding (`self.load`), and
1983 | `json.JSONEncoder` is used for encoding (`self.dump`). You can
1984 | customize the decoding or encoding process by inheriting from these
1985 | two classes and overriding their methods.
1986 |
1987 | @param object_hook
1988 | This function will be used to decode dictionaries. It takes a dictionary
1989 | as input, allows you to modify the dictionary or convert it to another
1990 | type of object, and then returns it. This allows you to customize the
1991 | data structure immediately after parsing JSON.
1992 |
1993 | @param parse_float
1994 | This function will be used to decode floating-point numbers in JSON. By
1995 | default, floating-point numbers are parsed into Python's float type. You
1996 | can change this behavior by providing a custom function.
1997 |
1998 | @param parse_int
1999 | This function will be used to decode integers in JSON. By default,
2000 | integers are parsed into Python's int type. You can change this behavior
2001 | by providing a custom function.
2002 |
2003 | @param parse_constant
2004 | This function will be used to decode special constants in JSON (such as
2005 | `Infinity`, `NaN`). By default, these constants are parsed into Python's
2006 | `float("inf")` and `float("nan")`. You can change this behavior by
2007 | providing a custom function.
2008 |
2009 | @param object_pairs_hook
2010 | This function will be used to decode JSON objects. It takes a list of
2011 | key-value pairs as input, allows you to convert these key-value pairs
2012 | into another type of object, and then returns it. For example, you can
2013 | use it to convert JSON objects to `gqylpy_dict.gdict`, which supports
2014 | accessing and modifying key-value pairs in the dictionary using the dot
2015 | operator.
2016 |
2017 | @param obj
2018 | The Python object you want to convert to JSON format and write to the
2019 | file.
2020 |
2021 | @param encoding
2022 | Specify the encoding for opening the file, usually specified as "UTF-8".
2023 | The default encoding is based on the platform, call
2024 | `locale.getpreferredencoding(False)` to get the current locale encoding.
2025 | See the `codecs` module for a list of supported encodings.
2026 |
2027 | @param skipkeys
2028 | If True (default is False), dictionary keys that are not of a basic
2029 | type (str, int, float, bool, None) will be skipped during the encoding
2030 | process.
2031 |
2032 | @param ensure_ascii
2033 | If True (default), all non-ASCII characters in the output will be
2034 | escaped. If False, these characters will be output as-is.
2035 |
2036 | @param check_circular
2037 | If True (default), the function will check for circular references in
2038 | the object and raise a `ValueError` if found. If False, no such check
2039 | will be performed.
2040 |
2041 | @param allow_nan
2042 | If True (default), `NaN`, `Infinity`, and `-Infinity` will be encoded as
2043 | JSON. If False, these values will raise a `ValueError`.
2044 |
2045 | @param indent
2046 | Specifies the number of spaces for indentation for prettier output. If
2047 | None (default), the most compact representation will be used.
2048 |
2049 | @param separators
2050 | A `(item_separator, key_separator)` tuple used to specify separators.
2051 | The default separators are `(", ", ": ")`. If the `indent` parameter is
2052 | specified, this parameter will be ignored.
2053 |
2054 | @param default
2055 | A function that will be used to convert objects that cannot be
2056 | serialized. This function should take an object as input and return a
2057 | serializable version.
2058 |
2059 | @param sort_keys
2060 | If True (default is False), the output of dictionaries will be sorted by
2061 | key order.
2062 | """
2063 |
2064 | def __init__(self, file: File, /):
2065 | self.file = file
2066 |
2067 | def load(
2068 | self,
2069 | *,
2070 | cls: Optional[Type['json.JSONDecoder']] = None,
2071 | object_hook: Optional[JsonObjectHook] = None,
2072 | parse_float: Optional[JsonObjectParse] = None,
2073 | parse_int: Optional[JsonObjectParse] = None,
2074 | parse_constant: Optional[JsonObjectParse] = None,
2075 | object_pairs_hook: Optional[JsonObjectPairsHook] = None
2076 | ) -> Any: ...
2077 |
2078 | def dump(
2079 | self,
2080 | obj: Any,
2081 | *,
2082 | encoding: Optional[str] = None,
2083 | skipkeys: Optional[bool] = None,
2084 | ensure_ascii: Optional[bool] = None,
2085 | check_circular: Optional[bool] = None,
2086 | allow_nan: Optional[bool] = None,
2087 | cls: Optional[Type['json.JSONEncoder']] = None,
2088 | indent: Optional[Union[int, str]] = None,
2089 | separators: Optional[Tuple[str, str]] = None,
2090 | default: Optional[Callable[[Any], Any]] = None,
2091 | sort_keys: Optional[bool] = None,
2092 | **kw
2093 | ) -> None: ...
2094 |
2095 |
2096 | class YAML:
2097 | """
2098 | A class for handling YAML operations with a file object. It provides
2099 | methods for loading YAML data from a file and dumping Python objects into a
2100 | file as YAML.
2101 |
2102 | @param loader
2103 | Specify a loader class to control how the YAML stream is parsed.
2104 | Defaults to `yaml.SafeLoader`. The YAML library provides different
2105 | loaders, each with specific uses and security considerations.
2106 |
2107 | `yaml.FullLoader`:
2108 | This is the default loader that can load the full range of YAML
2109 | functionality, including arbitrary Python objects. However, due to
2110 | its ability to load arbitrary Python objects, it may pose a security
2111 | risk as it can load and execute arbitrary Python code.
2112 |
2113 | `yaml.SafeLoader`:
2114 | This loader is safe, allowing only simple YAML tags to be loaded,
2115 | preventing the execution of arbitrary Python code. It is suitable
2116 | for loading untrusted or unknown YAML content.
2117 |
2118 | `yaml.Loader` & `yaml.UnsafeLoader`:
2119 | These loaders are similar to `FullLoader` but provide fewer security
2120 | guarantees. They allow loading of nearly all YAML tags, including
2121 | some that may execute arbitrary code.
2122 |
2123 | Through this parameter, you can choose which loader to use to balance
2124 | functionality and security. For example, if you are loading a fully
2125 | trusted YAML file and need to use the full range of YAML functionality,
2126 | you can choose `yaml.FullLoader`. If you are loading an unknown or not
2127 | fully trusted YAML file, you should choose `yaml.SafeLoader` to avoid
2128 | potential security risks.
2129 |
2130 | @param documents
2131 | A list of Python objects to serialize as YAML. Each object will be
2132 | serialized as a YAML document.
2133 |
2134 | @param dumper
2135 | An instance of a Dumper class used to serialize documents. If not
2136 | specified, the default `yaml.Dumper` class will be used.
2137 |
2138 | @param default_style
2139 | Used to define the style for strings in the output, default is None.
2140 | Options include ("|", ">", "|+", ">+"). Where "|" is used for literal
2141 | style and ">" is used for folded style.
2142 |
2143 | @param default_flow_style
2144 | A boolean value, default is False, specifying whether to use flow style
2145 | by default. Flow style is a compact representation that does not use the
2146 | traditional YAML block style for mappings and lists.
2147 |
2148 | @param canonical
2149 | A boolean value specifying whether to output canonical YAML. Canonical
2150 | YAML output is unique and does not depend on the Python object's
2151 | representation.
2152 |
2153 | @param indent
2154 | Used to specify the indentation level for block sequences and mappings.
2155 | The default is 2.
2156 |
2157 | @param width
2158 | Used to specify the width for folded styles. The default is 80.
2159 |
2160 | @param allow_unicode
2161 | A boolean value specifying whether Unicode characters are allowed in the
2162 | output.
2163 |
2164 | @param line_break
2165 | Specifies the line break character used in block styles. Can be None,
2166 | "\n", "\r", or "\r\n".
2167 |
2168 | @param encoding
2169 | Specify the output encoding, usually specified as "UTF-8". The default
2170 | encoding is based on the platform, call
2171 | `locale.getpreferredencoding(False)` to get the current locale encoding.
2172 | See the `codecs` module for a list of supported encodings.
2173 |
2174 | @param explicit_start
2175 | A boolean value specifying whether to include a YAML directive (`%YAML`)
2176 | in the output.
2177 |
2178 | @param explicit_end
2179 | A boolean value specifying whether to include an explicit document end
2180 | marker (...) in the output.
2181 |
2182 | @param version
2183 | Used to specify the YAML version as a tuple. Can be, for example,
2184 | `(1, 0)`, `(1, 1)`, or `(1, 2)`.
2185 |
2186 | @param tags
2187 | A dictionary used to map Python types to YAML tags
2188 |
2189 | @param sort_keys
2190 | A boolean value specifying whether to sort the keys of mappings in the
2191 | output. The default is True.
2192 | """
2193 |
2194 | def __init__(self, file: File, /):
2195 | self.file = file
2196 |
2197 | def load(self, loader: Optional['YamlLoader'] = None) -> Any: ...
2198 |
2199 | def load_all(
2200 | self, loader: Optional['YamlLoader'] = None
2201 | ) -> Iterator[Any]: ...
2202 |
2203 | def dump(
2204 | self,
2205 | data: Any,
2206 | /,
2207 | dumper: Optional['YamlDumper'] = None,
2208 | *,
2209 | default_style: Optional[str] = None,
2210 | default_flow_style: Optional[bool] = None,
2211 | canonical: Optional[bool] = None,
2212 | indent: Optional[int] = None,
2213 | width: Optional[int] = None,
2214 | allow_unicode: Optional[bool] = None,
2215 | line_break: Optional[str] = None,
2216 | encoding: Optional[str] = None,
2217 | explicit_start: Optional[bool] = None,
2218 | explicit_end: Optional[bool] = None,
2219 | version: Optional[Tuple[int, int]] = None,
2220 | tags: Optional[Mapping[str, str]] = None,
2221 | sort_keys: Optional[bool] = None
2222 | ) -> None: ...
2223 |
2224 | def dump_all(
2225 | self,
2226 | documents: Iterable[Any],
2227 | /,
2228 | dumper: Optional['YamlLoader'] = None,
2229 | *,
2230 | default_style: Optional[YamlDumpStyle] = None,
2231 | default_flow_style: Optional[bool] = None,
2232 | canonical: Optional[bool] = None,
2233 | indent: Optional[int] = None,
2234 | width: Optional[int] = None,
2235 | allow_unicode: Optional[bool] = None,
2236 | line_break: Optional[FileNewline] = None,
2237 | encoding: Optional[str] = None,
2238 | explicit_start: Optional[bool] = None,
2239 | explicit_end: Optional[bool] = None,
2240 | version: Optional[Tuple[int, int]] = None,
2241 | tags: Optional[Mapping[str, str]] = None,
2242 | sort_keys: Optional[bool] = None
2243 | ) -> None: ...
2244 |
2245 |
2246 | class SystemPath(Directory, File):
2247 |
2248 | def __init__(
2249 | self,
2250 | root: Optional[PathLink] = None,
2251 | /, *,
2252 | autoabs: Optional[bool] = None,
2253 | strict: Optional[bool] = None
2254 | ):
2255 | """
2256 | @param root
2257 | A path link, hopefully absolute. If it is a relative path, the
2258 | current working directory is used as the parent directory (the
2259 | return value of `os.getcwd()`). The default value is also the return
2260 | value of `os.getcwd()`.
2261 |
2262 | @param autoabs
2263 | Automatically normalize the path link and convert to absolute path,
2264 | at initialization. The default is False. It is always recommended
2265 | that you enable the parameter when the passed path is a relative
2266 | path.
2267 |
2268 | @param strict
2269 | Set to True to enable strict mode, which means that the passed path
2270 | must exist, otherwise raise `SystemPathNotFoundError` (or other).
2271 | The default is False.
2272 | """
2273 | super().__init__(root, autoabs=autoabs, strict=strict)
2274 |
2275 |
2276 | class _xe6_xad_x8c_xe7_x90_xaa_xe6_x80_xa1_xe7_x8e_xb2_xe8_x90_x8d_xe4_xba_x91:
2277 | gpack = globals()
2278 | gpath = f'{__name__}.i {__name__}'
2279 | gcode = __import__(gpath, fromlist=...)
2280 |
2281 | for gname in gpack:
2282 | if gname[0] != '_':
2283 | gfunc = getattr(gcode, gname, None)
2284 | if gfunc and getattr(gfunc, '__module__', None) == gpath:
2285 | gfunc.__module__ = __package__
2286 | gfunc.__doc__ = gpack[gname].__doc__
2287 | gpack[gname] = gfunc
2288 |
--------------------------------------------------------------------------------
/systempath/i systempath.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2022-2024 GQYLPY . All rights reserved.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | https://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | """
16 | import sys
17 | import csv
18 | import json
19 | import typing
20 | import hashlib
21 | import builtins
22 | import warnings
23 | import functools
24 |
25 | from copy import copy, deepcopy
26 | from configparser import ConfigParser
27 |
28 | from os import (
29 | stat, lstat, stat_result,
30 | rename, renames, replace, remove,
31 | chmod, access, truncate, utime,
32 | link, symlink, unlink, readlink,
33 | listdir, scandir, walk, chdir,
34 | mkdir, rmdir, makedirs, removedirs,
35 | getcwd, getcwdb
36 | )
37 |
38 | if sys.platform != 'win32':
39 | from os import mknod, chown, system, popen
40 |
41 | if sys.platform == 'linux':
42 | try:
43 | from os import getxattr, setxattr, listxattr, removexattr
44 | except ImportError:
45 | def getxattr(*a, **kw): raise NotImplementedError
46 | setxattr = listxattr = removexattr = getxattr
47 | try:
48 | from os import lchmod, lchown, chflags, lchflags
49 | except ImportError:
50 | def lchmod(*a, **kw): raise NotImplementedError
51 | lchown = chflags = lchflags = lchmod
52 | try:
53 | from pwd import getpwuid
54 | from grp import getgrgid
55 | except ModuleNotFoundError:
56 | def getpwuid(_): raise NotImplementedError
57 | getgrgid = getpwuid
58 |
59 | READ_BUFSIZE = 1024 * 64
60 | else:
61 | READ_BUFSIZE = 1024 * 1024
62 |
63 | from os.path import (
64 | basename, dirname, abspath, realpath, relpath,
65 | normpath, expanduser, expandvars,
66 | join, split, splitext, splitdrive, sep,
67 | isabs, exists, isdir, isfile, islink, ismount,
68 | getctime, getmtime, getatime, getsize
69 | )
70 |
71 | from shutil import move, copyfile, copytree, copystat, copymode, copy2, rmtree
72 |
73 | from stat import (
74 | S_ISDIR as s_isdir,
75 | S_ISREG as s_isreg,
76 | S_ISBLK as s_isblk,
77 | S_ISCHR as s_ischr,
78 | S_ISFIFO as s_isfifo
79 | )
80 |
81 | from _io import (
82 | FileIO, BufferedReader, BufferedWriter, BufferedRandom, TextIOWrapper,
83 | _BufferedIOBase as BufferedIOBase,
84 | DEFAULT_BUFFER_SIZE
85 | )
86 |
87 | from typing import (
88 | TypeVar, Type, Final, Literal, Optional, Union, Dict, Tuple, List, Mapping,
89 | Callable, Iterator, Iterable, Sequence, NoReturn, Any
90 | )
91 |
92 | if typing.TYPE_CHECKING:
93 | from _typeshed import SupportsWrite
94 | from configparser import Interpolation
95 |
96 | if sys.version_info >= (3, 9):
97 | from typing import Annotated
98 | else:
99 | class Annotated(metaclass=type('', (type,), {
100 | '__new__': lambda *a: type.__new__(*a)()
101 | })):
102 | def __getitem__(self, *a): ...
103 |
104 | if sys.version_info >= (3, 10):
105 | from typing import TypeAlias
106 | else:
107 | TypeAlias = TypeVar('TypeAlias')
108 |
109 | if sys.version_info >= (3, 11):
110 | from typing import Self
111 | else:
112 | Self = TypeVar('Self')
113 |
114 | try:
115 | import yaml
116 | except ModuleNotFoundError:
117 | yaml = None
118 | else:
119 | YamlLoader: TypeAlias = Union[
120 | Type[yaml.BaseLoader],
121 | Type[yaml.Loader],
122 | Type[yaml.FullLoader],
123 | Type[yaml.SafeLoader],
124 | Type[yaml.UnsafeLoader]
125 | ]
126 | YamlDumper: TypeAlias = Union[
127 | Type[yaml.BaseDumper],
128 | Type[yaml.Dumper],
129 | Type[yaml.SafeDumper]
130 | ]
131 |
132 | if basename(sys.argv[0]) != 'setup.py':
133 | import exceptionx as ex
134 |
135 | BytesOrStr: TypeAlias = Union[bytes, str]
136 | PathLink: TypeAlias = BytesOrStr
137 | PathType: TypeAlias = Union['Path', 'Directory', 'File', 'SystemPath']
138 | Closure: TypeAlias = TypeVar('Closure', bound=Callable)
139 | CopyFunction: TypeAlias = Callable[[PathLink, PathLink], None]
140 | CopyTreeIgnore: TypeAlias = \
141 | Callable[[PathLink, List[BytesOrStr]], List[BytesOrStr]]
142 |
143 | ConvertersMap: TypeAlias = Dict[str, Callable[[str], Any]]
144 | CSVDialectLike: TypeAlias = Union[str, csv.Dialect, Type[csv.Dialect]]
145 | JsonObjectHook: TypeAlias = Callable[[Dict[Any, Any]], Any]
146 | JsonObjectParse: TypeAlias = Callable[[str], Any]
147 | JsonObjectPairsHook: TypeAlias = Callable[[List[Tuple[Any, Any]]], Any]
148 | FileNewline: TypeAlias = Literal['', '\n', '\r', '\r\n']
149 | YamlDumpStyle: TypeAlias = Literal['|', '>', '|+', '>+']
150 |
151 | OpenMode: TypeAlias = Annotated[Literal[
152 | 'rb', 'rb_plus', 'rt', 'rt_plus', 'r', 'r_plus',
153 | 'wb', 'wb_plus', 'wt', 'wt_plus', 'w', 'w_plus',
154 | 'ab', 'ab_plus', 'at', 'at_plus', 'a', 'a_plus',
155 | 'xb', 'xb_plus', 'xt', 'xt_plus', 'x', 'x_plus'
156 | ], 'The file open mode.']
157 |
158 | EncodingErrorHandlingMode: TypeAlias = Annotated[Literal[
159 | 'strict',
160 | 'ignore',
161 | 'replace',
162 | 'surrogateescape',
163 | 'xmlcharrefreplace',
164 | 'backslashreplace',
165 | 'namereplace'
166 | ], 'The error handling modes for encoding and decoding (strictness).']
167 |
168 |
169 | class CSVReader(Iterator[List[str]]):
170 | line_num: int
171 | @property
172 | def dialect(self) -> csv.Dialect: ...
173 | def __next__(self) -> List[str]: ...
174 |
175 |
176 | class CSVWriter:
177 | @property
178 | def dialect(self) -> csv.Dialect: ...
179 | def writerow(self, row: Iterable[Any]) -> Any: ...
180 | def writerows(self, rows: Iterable[Iterable[Any]]) -> None: ...
181 |
182 |
183 | UNIQUE: Final[Annotated[object, 'A unique object.']] = object()
184 |
185 | sepb: Final[Annotated[bytes, 'The byte type path separator.']] = sep.encode()
186 |
187 |
188 | class MasqueradeClass(type):
189 | """
190 | Masquerade one class as another (default masquerade as first parent class).
191 | Warning, masquerade the class can cause unexpected problems, use caution.
192 | """
193 | __module__ = builtins.__name__
194 |
195 | __qualname__ = type.__qualname__
196 | # Warning, masquerade (modify) this attribute will cannot create the
197 | # portable serialized representation. In practice, however, this metaclass
198 | # often does not need to be serialized, so we try to ignore it.
199 |
200 | def __new__(mcs, __name__: str, __bases__: tuple, __dict__: dict):
201 | __masquerade_class__: Type[object] = __dict__.setdefault(
202 | '__masquerade_class__', __bases__[0] if __bases__ else object
203 | )
204 |
205 | if not isinstance(__masquerade_class__, type):
206 | raise TypeError('"__masquerade_class__" is not a class.')
207 |
208 | cls = type.__new__(
209 | mcs, __masquerade_class__.__name__, __bases__, __dict__
210 | )
211 |
212 | if cls.__module__ != __masquerade_class__.__module__:
213 | setattr(sys.modules[__masquerade_class__.__module__], __name__, cls)
214 |
215 | cls.__module__ = __masquerade_class__.__module__
216 | cls.__qualname__ = __masquerade_class__.__qualname__
217 |
218 | return cls
219 |
220 | def __hash__(cls) -> int:
221 | if sys._getframe(1).f_code in (deepcopy.__code__, copy.__code__):
222 | return type.__hash__(cls)
223 | return hash(cls.__masquerade_class__)
224 |
225 | def __eq__(cls, o) -> bool:
226 | return True if o is cls.__masquerade_class__ else type.__eq__(cls, o)
227 |
228 | def __init_subclass__(mcs) -> None:
229 | setattr(builtins, mcs.__name__, mcs)
230 | mcs.__name__ = MasqueradeClass.__name__
231 | mcs.__qualname__ = MasqueradeClass.__qualname__
232 | mcs.__module__ = MasqueradeClass.__module__
233 |
234 |
235 | MasqueradeClass.__name__ = type.__name__
236 | builtins.MasqueradeClass = MasqueradeClass
237 |
238 |
239 | class ReadOnlyMode(type, metaclass=MasqueradeClass):
240 | # Disallow modifying the attributes of the classes externally.
241 |
242 | def __setattr__(cls, name: str, value: Any) -> None:
243 | if sys._getframe(1).f_globals['__package__'] != __package__:
244 | raise ex.SetAttributeError(
245 | f'cannot set "{name}" attribute '
246 | f'of immutable type "{cls.__name__}".'
247 | )
248 | type.__setattr__(cls, name, value)
249 |
250 | def __delattr__(cls, name: str) -> NoReturn:
251 | raise ex.DeleteAttributeError(
252 | f'cannot delete "{name}" attribute '
253 | f'of immutable type "{cls.__name__}".'
254 | )
255 |
256 |
257 | class ReadOnly(metaclass=ReadOnlyMode):
258 | # Disallow modifying the attributes of the instances externally.
259 | __module__ = builtins.__name__
260 | __qualname__ = object.__name__
261 |
262 | # __dict__ = {}
263 | # Tamper with attribute `__dict__` to avoid modifying its subclass instance
264 | # attribute externally, but the serious problem is that it cannot
265 | # deserialize its subclass instance after tampering. Stop tampering for the
266 | # moment, the solution is still in the works.
267 |
268 | def __setattr__(self, name: str, value: Any) -> None:
269 | if sys._getframe(1).f_globals['__name__'] != __name__ and not \
270 | (isinstance(self, File) and name in ('content', 'contents')):
271 | raise ex.SetAttributeError(
272 | f'cannot set "{name}" attribute in instance '
273 | f'of immutable type "{self.__class__.__name__}".'
274 | )
275 | object.__setattr__(self, name, value)
276 |
277 | def __delattr__(self, name: str) -> None:
278 | if not isinstance(self, File) or name != 'content':
279 | raise ex.DeleteAttributeError(
280 | f'cannot delete "{name}" attribute in instance '
281 | f'of immutable type "{self.__class__.__name__}".'
282 | )
283 | object.__delattr__(self, name)
284 |
285 |
286 | ReadOnly.__name__ = object.__name__
287 | builtins.ReadOnly = ReadOnly
288 |
289 |
290 | def dst2abs(func: Callable) -> Closure:
291 | # If the destination path is relative and is a single name, the parent path
292 | # of the source is used as the parent path of the destination instead of
293 | # using the current working directory, different from the traditional way.
294 | @functools.wraps(func)
295 | def core(path: PathType, dst: PathLink) -> PathLink:
296 | try:
297 | singlename: bool = basename(dst) == dst
298 | except TypeError:
299 | raise ex.DestinationPathTypeError(
300 | 'destination path type can only be "bytes" or "str", '
301 | f'not "{dst.__class__.__name__}".'
302 | ) from None
303 | if singlename:
304 | try:
305 | dst: PathLink = join(dirname(path), dst)
306 | except TypeError as e:
307 | if dst.__class__ is bytes:
308 | name: bytes = path.name.encode()
309 | elif dst.__class__ is str:
310 | name: str = path.name.decode()
311 | else:
312 | raise e from None
313 | dst: PathLink = join(dirname(name), dst)
314 | func(path, dst)
315 | path.name = dst
316 | return dst
317 | return core
318 |
319 |
320 | def joinpath(func: Callable) -> Closure:
321 | global BytesOrStr
322 | # Compatible with Python earlier versions.
323 |
324 | @functools.wraps(func)
325 | def core(path: PathType, name: BytesOrStr, /) -> Any:
326 | try:
327 | name: PathLink = join(path, name)
328 | except TypeError:
329 | if name.__class__ is bytes:
330 | name: str = name.decode()
331 | elif name.__class__ is str:
332 | name: bytes = name.encode()
333 | else:
334 | raise
335 | name: PathLink = join(path, name)
336 | return func(path, name)
337 |
338 | return core
339 |
340 |
341 | def ignore_error(e) -> bool:
342 | return (
343 | getattr(e, 'errno', None) in (2, 20, 9, 10062)
344 | or
345 | getattr(e, 'winerror', None) in (21, 123, 1921)
346 | )
347 |
348 |
349 | def testpath(testfunc: Callable[[int], bool], path: PathType) -> bool:
350 | try:
351 | return testfunc(path.stat.st_mode)
352 | except OSError as e:
353 | # Path does not exist or is a broken symlink.
354 | if not ignore_error(e):
355 | raise
356 | return False
357 | except ValueError:
358 | # Non-encodable path.
359 | return False
360 |
361 |
362 | class Path(ReadOnly):
363 |
364 | def __new__(cls, name: PathLink = UNIQUE, /, *, strict: bool = False, **kw):
365 | # Compatible object deserialization.
366 | if name is not UNIQUE:
367 | if name.__class__ not in (bytes, str):
368 | raise ex.NotAPathError(
369 | 'path type can only be "bytes" or "str", '
370 | f'not "{name.__class__.__name__}".'
371 | )
372 | if strict and not exists(name):
373 | raise ex.SystemPathNotFoundError(
374 | f'system path {name!r} does not exist.'
375 | )
376 | return object.__new__(cls)
377 |
378 | def __init__(
379 | self,
380 | name: PathLink,
381 | /, *,
382 | autoabs: bool = False,
383 | strict: bool = False,
384 | dir_fd: Optional[int] = None,
385 | follow_symlinks: bool = True
386 | ):
387 | self.name = abspath(name) if autoabs else name
388 | self.strict = strict
389 | self.dir_fd = dir_fd
390 | self.follow_symlinks = follow_symlinks
391 |
392 | def __str__(self) -> str:
393 | return self.name if self.name.__class__ is str else repr(self.name)
394 |
395 | def __repr__(self) -> str:
396 | return f'<{__package__}.{self.__class__.__name__} name={self.name!r}>'
397 |
398 | def __bytes__(self) -> bytes:
399 | return self.name if self.name.__class__ is bytes else self.name.encode()
400 |
401 | def __eq__(self, other: [PathType, PathLink], /) -> bool:
402 | if self is other:
403 | return True
404 |
405 | if isinstance(other, Path):
406 | other_type = other.__class__
407 | other_path = abspath(other.name)
408 | other_dir_fd = other.dir_fd
409 | elif other.__class__ in (bytes, str):
410 | other_type = Path
411 | other_path = abspath(other)
412 | other_dir_fd = None
413 | else:
414 | return False
415 |
416 | if self.name.__class__ is not other_path.__class__:
417 | other_path = other_path.encode() \
418 | if other_path.__class__ is str else other_path.decode()
419 |
420 | return any((
421 | self.__class__ == other_type,
422 | self.__class__ in (Path, SystemPath),
423 | other_type in (Path, SystemPath)
424 | )) and abspath(self) == other_path and self.dir_fd == other_dir_fd
425 |
426 | def __len__(self) -> int:
427 | return len(self.name)
428 |
429 | def __bool__(self) -> bool:
430 | return self.exists
431 |
432 | def __fspath__(self) -> PathLink:
433 | return self.name
434 |
435 | def __truediv__(self, subpath: Union[PathType, PathLink], /) -> PathType:
436 | if isinstance(subpath, Path):
437 | subpath: PathLink = subpath.name
438 | try:
439 | joined_path: PathLink = join(self, subpath)
440 | except TypeError:
441 | if subpath.__class__ is bytes:
442 | subpath: str = subpath.decode()
443 | elif subpath.__class__ is str:
444 | subpath: bytes = subpath.encode()
445 | else:
446 | raise ex.NotAPathError(
447 | 'right path can only be an instance of '
448 | f'"{__package__}.{Path.__name__}" or a path link, '
449 | f'not "{subpath.__class__.__name__}".'
450 | ) from None
451 | joined_path: PathLink = join(self, subpath)
452 |
453 | if self.strict:
454 | if isfile(joined_path):
455 | pathtype = File
456 | elif isdir(joined_path):
457 | pathtype = Directory
458 | elif self.__class__ is Path:
459 | pathtype = Path
460 | else:
461 | pathtype = SystemPath
462 | elif self.__class__ is Path:
463 | pathtype = Path
464 | else:
465 | pathtype = SystemPath
466 |
467 | return pathtype(
468 | joined_path,
469 | strict=self.strict,
470 | dir_fd=self.dir_fd,
471 | follow_symlinks=self.follow_symlinks
472 | )
473 |
474 | def __rtruediv__(self, dirpath: PathLink, /) -> PathType:
475 | try:
476 | joined_path: PathLink = join(dirpath, self)
477 | except TypeError:
478 | if dirpath.__class__ is bytes:
479 | dirpath: str = dirpath.decode()
480 | elif dirpath.__class__ is str:
481 | dirpath: bytes = dirpath.encode()
482 | else:
483 | raise ex.NotAPathError(
484 | 'left path type can only be "bytes" or "str", '
485 | f'not "{dirpath.__class__.__name__}".'
486 | ) from None
487 | joined_path: PathLink = join(dirpath, self)
488 | return self.__class__(
489 | joined_path,
490 | strict=self.strict,
491 | follow_symlinks=self.follow_symlinks
492 | )
493 |
494 | def __add__(self, subpath: Union[PathType, PathLink], /) -> PathType:
495 | return self.__truediv__(subpath)
496 |
497 | def __radd__(self, dirpath: PathLink, /) -> PathType:
498 | return self.__rtruediv__(dirpath)
499 |
500 | @property
501 | def basename(self) -> BytesOrStr:
502 | return basename(self)
503 |
504 | @property
505 | def dirname(self) -> 'Directory':
506 | return Directory(
507 | dirname(self),
508 | strict=self.strict,
509 | dir_fd=self.dir_fd,
510 | follow_symlinks=self.follow_symlinks
511 | )
512 |
513 | def dirnamel(self, level: int) -> 'Directory':
514 | directory = self
515 | for _ in range(level):
516 | directory: PathLink = dirname(directory)
517 | return Directory(
518 | directory,
519 | strict=self.strict,
520 | dir_fd=self.dir_fd,
521 | follow_symlinks=self.follow_symlinks
522 | )
523 |
524 | def ldirname(self, *, level: int = 1) -> PathType:
525 | sepx: BytesOrStr = sepb if self.name.__class__ is bytes else sep
526 | return Directory(sepx.join(self.name.split(sepx)[level:]))
527 |
528 | @property
529 | def abspath(self) -> PathType:
530 | return self.__class__(
531 | abspath(self),
532 | strict=self.strict,
533 | follow_symlinks=self.follow_symlinks
534 | )
535 |
536 | def realpath(self, *, strict: bool = False) -> PathType:
537 | return self.__class__(
538 | realpath(self, strict=strict),
539 | strict=self.strict,
540 | follow_symlinks=self.follow_symlinks
541 | )
542 |
543 | def relpath(self, start: Optional[PathLink] = None) -> PathType:
544 | return self.__class__(
545 | relpath(self, start=start),
546 | strict=self.strict,
547 | follow_symlinks=self.follow_symlinks
548 | )
549 |
550 | def normpath(self) -> PathType:
551 | return self.__class__(
552 | normpath(self),
553 | strict=self.strict,
554 | dir_fd=self.dir_fd,
555 | follow_symlinks=self.follow_symlinks
556 | )
557 |
558 | def expanduser(self) -> PathType:
559 | return self.__class__(
560 | expanduser(self),
561 | strict=self.strict,
562 | follow_symlinks=self.follow_symlinks
563 | )
564 |
565 | def expandvars(self) -> PathType:
566 | return self.__class__(
567 | expandvars(self),
568 | strict=self.strict,
569 | follow_symlinks=self.follow_symlinks
570 | )
571 |
572 | def split(self) -> Tuple[PathLink, BytesOrStr]:
573 | return split(self)
574 |
575 | def splitdrive(self) -> Tuple[BytesOrStr, PathLink]:
576 | return splitdrive(self)
577 |
578 | @property
579 | def isabs(self) -> bool:
580 | return isabs(self)
581 |
582 | @property
583 | def exists(self) -> bool:
584 | try:
585 | self.stat
586 | except OSError as e:
587 | if not ignore_error(e):
588 | raise
589 | return False
590 | except ValueError:
591 | return False
592 | return True
593 |
594 | @property
595 | def lexists(self) -> bool:
596 | try:
597 | self.lstat
598 | except OSError as e:
599 | if not ignore_error(e):
600 | raise
601 | return False
602 | except ValueError:
603 | return False
604 | return True
605 |
606 | @property
607 | def isdir(self) -> bool:
608 | return testpath(s_isdir, self)
609 |
610 | @property
611 | def isfile(self) -> bool:
612 | return testpath(s_isreg, self)
613 |
614 | @property
615 | def islink(self) -> bool:
616 | return islink(self)
617 |
618 | @property
619 | def ismount(self) -> bool:
620 | return ismount(self)
621 |
622 | @property
623 | def is_block_device(self) -> bool:
624 | return testpath(s_isblk, self)
625 |
626 | @property
627 | def is_char_device(self) -> bool:
628 | return testpath(s_ischr, self)
629 |
630 | @property
631 | def isfifo(self) -> bool:
632 | return testpath(s_isfifo, self)
633 |
634 | @property
635 | def isempty(self) -> bool:
636 | if self.isdir:
637 | return not bool(listdir(self))
638 | if self.isfile:
639 | return not bool(getsize(self))
640 | if self.exists:
641 | raise ex.NotADirectoryOrFileError(repr(self.name))
642 |
643 | raise ex.SystemPathNotFoundError(
644 | f'system path {self.name!r} does not exist.'
645 | )
646 |
647 | @property
648 | def readable(self) -> bool:
649 | return access(
650 | self, 4, dir_fd=self.dir_fd, follow_symlinks=self.follow_symlinks
651 | )
652 |
653 | @property
654 | def writeable(self) -> bool:
655 | return access(
656 | self, 2, dir_fd=self.dir_fd, follow_symlinks=self.follow_symlinks
657 | )
658 |
659 | @property
660 | def executable(self) -> bool:
661 | return access(
662 | self, 1, dir_fd=self.dir_fd, follow_symlinks=self.follow_symlinks
663 | )
664 |
665 | def delete(
666 | self,
667 | *,
668 | ignore_errors: bool = False,
669 | onerror: Optional[Callable] = None
670 | ) -> None:
671 | if self.isdir:
672 | rmtree(self, ignore_errors=ignore_errors, onerror=onerror)
673 | else:
674 | try:
675 | remove(self)
676 | except FileNotFoundError:
677 | if not ignore_errors:
678 | raise
679 |
680 | @dst2abs
681 | def rename(self, dst: PathLink, /) -> None:
682 | rename(self, dst, src_dir_fd=self.dir_fd, dst_dir_fd=self.dir_fd)
683 |
684 | @dst2abs
685 | def renames(self, dst: PathLink, /) -> None:
686 | renames(self, dst)
687 |
688 | @dst2abs
689 | def replace(self, dst: PathLink, /) -> None:
690 | replace(self, dst, src_dir_fd=self.dir_fd, dst_dir_fd=self.dir_fd)
691 |
692 | def move(
693 | self,
694 | dst: Union[PathType, PathLink],
695 | /, *,
696 | copy_function: Callable[[PathLink, PathLink], None] = copy2
697 | ) -> None:
698 | move(self, dst, copy_function=copy_function)
699 |
700 | def copystat(self, dst: Union[PathType, PathLink], /) -> None:
701 | copystat(self, dst, follow_symlinks=self.follow_symlinks)
702 |
703 | def copymode(self, dst: Union[PathType, PathLink], /) -> None:
704 | copymode(self, dst, follow_symlinks=self.follow_symlinks)
705 |
706 | def symlink(self, dst: Union[PathType, PathLink], /) -> None:
707 | symlink(self, dst, dir_fd=self.dir_fd)
708 |
709 | def readlink(self) -> PathLink:
710 | return readlink(self, dir_fd=self.dir_fd)
711 |
712 | @property
713 | def stat(self) -> stat_result:
714 | return stat(
715 | self, dir_fd=self.dir_fd, follow_symlinks=self.follow_symlinks
716 | )
717 |
718 | @property
719 | def lstat(self) -> stat_result:
720 | return lstat(self, dir_fd=self.dir_fd)
721 |
722 | def getsize(self) -> int:
723 | return getsize(self)
724 |
725 | def getctime(self) -> float:
726 | return getctime(self)
727 |
728 | def getmtime(self) -> float:
729 | return getmtime(self)
730 |
731 | def getatime(self) -> float:
732 | return getatime(self)
733 |
734 | def chmod(self, mode: int, /) -> None:
735 | chmod(
736 | self, mode, dir_fd=self.dir_fd, follow_symlinks=self.follow_symlinks
737 | )
738 |
739 | def access(self, mode: int, /, *, effective_ids: bool = False) -> bool:
740 | return access(
741 | self, mode,
742 | dir_fd=self.dir_fd,
743 | effective_ids=effective_ids,
744 | follow_symlinks=self.follow_symlinks
745 | )
746 |
747 | if sys.platform != 'win32':
748 | def lchmod(self, mode: int, /) -> None:
749 | lchmod(self, mode)
750 |
751 | @property
752 | def owner(self) -> str:
753 | return getpwuid(self.stat.st_uid).pw_name
754 |
755 | @property
756 | def group(self) -> str:
757 | return getgrgid(self.stat.st_gid).gr_name
758 |
759 | def chown(self, uid: int, gid: int) -> None:
760 | return chown(
761 | self, uid, gid,
762 | dir_fd=self.dir_fd,
763 | follow_symlinks=self.follow_symlinks
764 | )
765 |
766 | def lchown(self, uid: int, gid: int) -> None:
767 | lchown(self, uid, gid)
768 |
769 | def chflags(self, flags: int) -> None:
770 | chflags(self, flags, follow_symlinks=self.follow_symlinks)
771 |
772 | def lchflags(self, flags: int) -> None:
773 | lchflags(self, flags)
774 |
775 | def chattr(self, operator: Literal['+', '-', '='], attrs: str) -> None:
776 | warnings.warn(
777 | 'implementation of method `chattr` is to directly call the '
778 | 'system command `chattr`, so this is very unreliable.'
779 | , stacklevel=2)
780 | if operator not in ('+', '-', '='):
781 | raise ex.ChattrError(
782 | f'unsupported operation "{operator}", only "+", "-" or "=".'
783 | )
784 | pathlink: str = self.name if self.name.__class__ is str \
785 | else self.name.decode()
786 | c: str = f'chattr {operator}{attrs} {pathlink}'
787 | if system(f'sudo {c} &>/dev/null'):
788 | raise ex.ChattrError(c)
789 |
790 | def lsattr(self) -> str:
791 | warnings.warn(
792 | 'implementation of method `lsattr` is to directly call the '
793 | 'system command `lsattr`, so this is very unreliable.'
794 | , stacklevel=2)
795 | pathlink: str = self.name if self.name.__class__ is str \
796 | else self.name.decode()
797 | c: str = f'lsattr {pathlink}'
798 | attrs: str = popen(
799 | "sudo %s 2>/dev/null | awk '{print $1}'" % c
800 | ).read()[:-1]
801 | if len(attrs) != 16:
802 | raise ex.LsattrError(c)
803 | return attrs
804 |
805 | def exattr(self, attr: str, /) -> bool:
806 | return attr in self.lsattr()
807 |
808 | if sys.platform == 'linux':
809 | def getxattr(self, attribute: BytesOrStr, /) -> bytes:
810 | return getxattr(
811 | self, attribute, follow_symlinks=self.follow_symlinks
812 | )
813 |
814 | def setxattr(
815 | self, attribute: BytesOrStr, value: bytes, *, flags: int = 0
816 | ) -> None:
817 | setxattr(
818 | self, attribute, value, flags,
819 | follow_symlinks=self.follow_symlinks
820 | )
821 |
822 | def listxattr(self) -> List[str]:
823 | return listxattr(self, follow_symlinks=self.follow_symlinks)
824 |
825 | def removexattr(self, attribute: BytesOrStr, /) -> None:
826 | removexattr(
827 | self, attribute, follow_symlinks=self.follow_symlinks
828 | )
829 |
830 | def utime(
831 | self,
832 | /,
833 | times: Optional[Tuple[Union[int, float], Union[int, float]]] = None
834 | ) -> None:
835 | utime(
836 | self, times,
837 | dir_fd=self.dir_fd,
838 | follow_symlinks=self.follow_symlinks
839 | )
840 |
841 |
842 | class Directory(Path):
843 |
844 | def __new__(
845 | cls, name: PathLink = '.', /, *, strict: bool = False, **kw
846 | ) -> 'Directory':
847 | instance = Path.__new__(cls, name, strict=strict, **kw)
848 |
849 | if strict and not isdir(name):
850 | raise NotADirectoryError(
851 | f'system path {name!r} is not a directory.'
852 | )
853 |
854 | return instance
855 |
856 | @joinpath
857 | def __getitem__(self, name: PathLink) -> PathType:
858 | if self.strict:
859 | if isdir(name):
860 | return Directory(name, strict=self.strict)
861 | if isfile(name):
862 | return File(name)
863 | if exists(name):
864 | return Path(name)
865 | raise ex.SystemPathNotFoundError(
866 | f'system path {name!r} does not exist.'
867 | )
868 | return SystemPath(name)
869 |
870 | @joinpath
871 | def __delitem__(self, path: PathLink) -> None:
872 | Path(path).delete()
873 |
874 | def __iter__(self) -> Iterator[Union['Directory', 'File', Path]]:
875 | for name in listdir(self):
876 | path: PathLink = join(self, name)
877 | yield Directory(path) if isdir(path) else \
878 | File(path) if isfile(path) else Path(path)
879 |
880 | def __bool__(self) -> bool:
881 | return self.isdir
882 |
883 | @staticmethod
884 | def home(
885 | *, strict: bool = False, follow_symlinks: bool = True
886 | ) -> 'Directory':
887 | return Directory(
888 | expanduser('~'), strict=strict, follow_symlinks=follow_symlinks
889 | )
890 |
891 | @property
892 | def subpaths(self) -> Iterator[Union['Directory', 'File', Path]]:
893 | return self.__iter__()
894 |
895 | @property
896 | def subpath_names(self) -> List[BytesOrStr]:
897 | return listdir(self)
898 |
899 | def scandir(self) -> Iterator:
900 | return scandir(self)
901 |
902 | def tree(
903 | self,
904 | *,
905 | level: int = float('inf'),
906 | downtop: Optional[bool] = None,
907 | bottom_up: bool = UNIQUE,
908 | omit_dir: bool = False,
909 | pure_path: Optional[bool] = None,
910 | mysophobia: bool = UNIQUE,
911 | shortpath: bool = False
912 | ) -> Iterator[Union[Path, PathLink]]:
913 | return tree(
914 | self.name,
915 | level =level,
916 | downtop =downtop,
917 | bottom_up =bottom_up,
918 | omit_dir =omit_dir,
919 | pure_path =pure_path,
920 | mysophobia=mysophobia,
921 | shortpath =shortpath
922 | )
923 |
924 | def walk(
925 | self, *, topdown: bool = True, onerror: Optional[Callable] = None
926 | ) -> Iterator[Tuple[PathLink, List[BytesOrStr], List[BytesOrStr]]]:
927 | return walk(
928 | self,
929 | topdown=topdown,
930 | onerror=onerror,
931 | followlinks=not self.follow_symlinks
932 | )
933 |
934 | def search(
935 | self,
936 | slicing: BytesOrStr,
937 | /, *,
938 | level: int = float('inf'),
939 | omit_dir: bool = False,
940 | pure_path: Optional[bool] = None,
941 | shortpath: bool = False
942 | ) -> Iterator[Union[PathType, PathLink]]:
943 | slicing: BytesOrStr = normpath(slicing)
944 | nullchar: BytesOrStr = b'' if self.name.__class__ is bytes else ''
945 | dirtree = tree(
946 | self.name, level=level, omit_dir=omit_dir,
947 | pure_path=pure_path, shortpath=shortpath
948 | )
949 | for subpath in dirtree:
950 | pure_subpath = (subpath if pure_path else subpath.name)\
951 | .replace(self.name, nullchar)[1:]
952 | try:
953 | r: bool = slicing in pure_subpath
954 | except TypeError:
955 | if slicing.__class__ is bytes:
956 | slicing: str = slicing.decode()
957 | elif slicing.__class__ is str:
958 | slicing: bytes = slicing.encode()
959 | else:
960 | raise ex.ParameterError(
961 | 'parameter "slicing" must be of type bytes or str,'
962 | f'not "{slicing.__class__.__name__}".'
963 | ) from None
964 | r: bool = slicing in pure_subpath
965 | if r:
966 | yield subpath
967 |
968 | def copytree(
969 | self,
970 | dst: Union['Directory', PathLink],
971 | /, *,
972 | symlinks: bool = False,
973 | ignore: Optional[CopyTreeIgnore] = None,
974 | copy_function: CopyFunction = copy2,
975 | ignore_dangling_symlinks: bool = False,
976 | dirs_exist_ok: bool = False
977 | ) -> None:
978 | copytree(
979 | self, dst,
980 | symlinks =symlinks,
981 | ignore =ignore,
982 | copy_function =copy_function,
983 | ignore_dangling_symlinks=ignore_dangling_symlinks,
984 | dirs_exist_ok =dirs_exist_ok
985 | )
986 |
987 | def clear(
988 | self,
989 | *,
990 | ignore_errors: bool = False,
991 | onerror: Optional[Callable] = None
992 | ) -> None:
993 | for name in listdir(self):
994 | path: PathLink = join(self, name)
995 | if isdir(path):
996 | rmtree(path, ignore_errors=ignore_errors, onerror=onerror)
997 | else:
998 | try:
999 | remove(self)
1000 | except FileNotFoundError:
1001 | if not ignore_errors:
1002 | raise
1003 |
1004 | def mkdir(self, mode: int = 0o777, *, ignore_exists: bool = False) -> None:
1005 | try:
1006 | mkdir(self, mode)
1007 | except FileExistsError:
1008 | if not ignore_exists:
1009 | raise
1010 |
1011 | def makedirs(self, mode: int = 0o777, *, exist_ok: bool = False) -> None:
1012 | makedirs(self, mode, exist_ok=exist_ok)
1013 |
1014 | def rmdir(self) -> None:
1015 | rmdir(self)
1016 |
1017 | def removedirs(self) -> None:
1018 | removedirs(self)
1019 |
1020 | def rmtree(
1021 | self,
1022 | *,
1023 | ignore_errors: bool = False,
1024 | onerror: Optional[Callable] = None
1025 | ) -> None:
1026 | rmtree(self, ignore_errors=ignore_errors, onerror=onerror)
1027 |
1028 | @property
1029 | def isempty(self) -> bool:
1030 | return not bool(listdir(self))
1031 |
1032 | def chdir(self) -> None:
1033 | chdir(self)
1034 |
1035 |
1036 | class File(Path):
1037 |
1038 | def __new__(cls, name: PathLink = UNIQUE, /, *, strict: bool = False, **kw):
1039 | instance = Path.__new__(cls, name, strict=strict, **kw)
1040 |
1041 | if strict and not isfile(name):
1042 | raise ex.NotAFileError(f'system path {name!r} is not a file.')
1043 |
1044 | return instance
1045 |
1046 | def __bool__(self) -> bool:
1047 | return self.isfile
1048 |
1049 | def __contains__(self, subcontent: bytes, /) -> bool:
1050 | return Content(self).contains(subcontent)
1051 |
1052 | def __iter__(self) -> Iterator[bytes]:
1053 | yield from Content(self)
1054 |
1055 | def __truediv__(self, other: Any, /) -> NoReturn:
1056 | x: str = __package__ + '.' + File.__name__
1057 | y: str = other.__class__.__name__
1058 | if hasattr(other, '__module__'):
1059 | y: str = other.__module__ + '.' + y
1060 | raise TypeError(f'unsupported operand type(s) for /: "{x}" and "{y}".')
1061 |
1062 | @property
1063 | def open(self) -> 'Open':
1064 | return Open(self)
1065 |
1066 | @property
1067 | def ini(self) -> 'INI':
1068 | return INI(self)
1069 |
1070 | @property
1071 | def csv(self) -> 'CSV':
1072 | return CSV(self)
1073 |
1074 | @property
1075 | def json(self) -> 'JSON':
1076 | return JSON(self)
1077 |
1078 | @property
1079 | def yaml(self) -> 'YAML':
1080 | return YAML(self)
1081 |
1082 | @property
1083 | def content(self) -> bytes:
1084 | return FileIO(self).read()
1085 |
1086 | @content.setter
1087 | def content(self, content: bytes, /) -> None:
1088 | if content.__class__ is not bytes:
1089 | # Beware of original data loss due to write failures (the `content`
1090 | # type error).
1091 | raise TypeError(
1092 | 'content type to be written can only be "bytes", '
1093 | f'not "{content.__class__.__name__}".'
1094 | )
1095 | FileIO(self, 'wb').write(content)
1096 |
1097 | @content.deleter
1098 | def content(self) -> None:
1099 | truncate(self, 0)
1100 |
1101 | @property
1102 | def contents(self) -> 'Content':
1103 | return Content(self)
1104 |
1105 | @contents.setter
1106 | def contents(self, content: ['Content', bytes]) -> None:
1107 | # For compatible with `Content.__iadd__` and `Content.__ior__`.
1108 | pass
1109 |
1110 | def splitext(self) -> Tuple[BytesOrStr, BytesOrStr]:
1111 | return splitext(self)
1112 |
1113 | @property
1114 | def extension(self) -> BytesOrStr:
1115 | return splitext(self)[1]
1116 |
1117 | def copy(self, dst: Union[PathType, PathLink], /) -> None:
1118 | copyfile(self, dst, follow_symlinks=self.follow_symlinks)
1119 |
1120 | def copycontent(
1121 | self,
1122 | other: Union['File', 'SupportsWrite[bytes]'],
1123 | /, *,
1124 | bufsize: int = READ_BUFSIZE
1125 | ) -> Union['File', 'SupportsWrite[bytes]']:
1126 | write, read = (
1127 | FileIO(other, 'wb') if isinstance(other, File) else other
1128 | ).write, FileIO(self).read
1129 |
1130 | while True:
1131 | content = read(bufsize)
1132 | if not content:
1133 | break
1134 | write(content)
1135 |
1136 | return other
1137 |
1138 | def link(self, dst: Union[PathType, PathLink], /) -> None:
1139 | link(
1140 | self, dst,
1141 | src_dir_fd=self.dir_fd,
1142 | dst_dir_fd=self.dir_fd,
1143 | follow_symlinks=self.follow_symlinks
1144 | )
1145 |
1146 | @property
1147 | def isempty(self) -> bool:
1148 | return not bool(getsize(self))
1149 |
1150 | if sys.platform == 'win32':
1151 | def mknod(
1152 | self,
1153 | mode: int = 0o600,
1154 | *,
1155 | ignore_exists: bool = False,
1156 | **__
1157 | ) -> None:
1158 | try:
1159 | FileIO(self, 'xb')
1160 | except FileExistsError:
1161 | if not ignore_exists:
1162 | raise
1163 | else:
1164 | chmod(self, mode)
1165 | else:
1166 | def mknod(
1167 | self,
1168 | mode: int = None,
1169 | *,
1170 | device: int = 0,
1171 | ignore_exists: bool = False
1172 | ) -> None:
1173 | try:
1174 | mknod(self, mode, device, dir_fd=self.dir_fd)
1175 | except FileExistsError:
1176 | if not ignore_exists:
1177 | raise
1178 |
1179 | def mknods(
1180 | self,
1181 | mode: int = 0o600 if sys.platform == 'win32' else None,
1182 | *,
1183 | device: int = 0,
1184 | ignore_exists: bool = False
1185 | ) -> None:
1186 | parentdir: PathLink = dirname(self)
1187 | if not (parentdir in ('', b'') or exists(parentdir)):
1188 | makedirs(parentdir, mode, exist_ok=True)
1189 | self.mknod(mode, device=device, ignore_exists=ignore_exists)
1190 |
1191 | def remove(self, *, ignore_errors: bool = False) -> None:
1192 | try:
1193 | remove(self)
1194 | except FileNotFoundError:
1195 | if not ignore_errors:
1196 | raise
1197 |
1198 | def unlink(self) -> None:
1199 | unlink(self, dir_fd=self.dir_fd)
1200 |
1201 | def contains(self, subcontent: bytes, /) -> bool:
1202 | return Content(self).contains(subcontent)
1203 |
1204 | def truncate(self, length: int) -> None:
1205 | truncate(self, length)
1206 |
1207 | def clear(self) -> None:
1208 | truncate(self, 0)
1209 |
1210 | def md5(self, salting: bytes = b'') -> str:
1211 | return Content(self).md5(salting)
1212 |
1213 | def read(
1214 | self, size: int = -1, /, *, encoding: Optional[str] = None, **kw
1215 | ) -> str:
1216 | return Open(self).r(encoding=encoding, **kw).read(size)
1217 |
1218 | def write(
1219 | self, content: str, /, *, encoding: Optional[str] = None, **kw
1220 | ) -> int:
1221 | return Open(self).w().write(content, **kw)
1222 |
1223 | def append(
1224 | self, content: str, /, *, encoding: Optional[str] = None, **kw
1225 | ) -> int:
1226 | return Open(self).a().write(content, **kw)
1227 |
1228 | create = mknod
1229 | creates = mknods
1230 |
1231 |
1232 | class Open(ReadOnly):
1233 | __modes__ = {
1234 | 'r': BufferedReader,
1235 | 'w': BufferedWriter,
1236 | 'x': BufferedWriter,
1237 | 'a': BufferedWriter
1238 | }
1239 | for mode in tuple(__modes__):
1240 | mode_b, mode_t = mode + 'b', mode + 't'
1241 | __modes__[mode_b] = __modes__[mode_t] = __modes__[mode]
1242 | __modes__[mode + '_plus'] = BufferedRandom
1243 | __modes__[mode_b + '_plus'] = BufferedRandom
1244 | __modes__[mode_t + '_plus'] = BufferedRandom
1245 | del mode, mode_b, mode_t
1246 |
1247 | def __init__(self, file: Union[File, PathLink], /):
1248 | if not isinstance(file, (File, bytes, str)):
1249 | raise ex.NotAFileError(
1250 | 'file can only be an instance of '
1251 | f'"{__package__}.{File.__name__}" or a path link, '
1252 | f'not "{file.__class__.__name__}".'
1253 | )
1254 | self.file = file
1255 |
1256 | def __getattr__(self, mode: OpenMode, /) -> Closure:
1257 | try:
1258 | buffer: Type[BufferedIOBase] = Open.__modes__[mode]
1259 | except KeyError:
1260 | raise AttributeError(
1261 | f"'{self.__class__.__name__}' object has no attribute '{mode}'"
1262 | ) from None
1263 | return self.__open__(buffer, mode)
1264 |
1265 | def __dir__(self) -> Iterable[str]:
1266 | methods = object.__dir__(self)
1267 | methods.remove('__modes__')
1268 | methods.remove('__pass__')
1269 | methods += self.__modes__
1270 | return methods
1271 |
1272 | def __repr__(self) -> str:
1273 | filelink: PathLink = \
1274 | self.file.name if isinstance(self.file, File) else self.file
1275 | return f'<{__package__}.{self.__class__.__name__} file={filelink!r}>'
1276 |
1277 | def __open__(self, buffer: Type[BufferedIOBase], mode: OpenMode) -> Closure:
1278 | def init_buffer_instance(
1279 | *,
1280 | bufsize: int = DEFAULT_BUFFER_SIZE,
1281 | encoding: Optional[str] = None,
1282 | errors: Optional[EncodingErrorHandlingMode] = None,
1283 | newline: Optional[str] = None,
1284 | line_buffering: bool = False,
1285 | write_through: bool = False,
1286 | opener: Optional[Callable[[PathLink, int], int]] = None
1287 | ) -> Union[BufferedIOBase, TextIOWrapper]:
1288 | buf: BufferedIOBase = buffer(
1289 | raw=FileIO(
1290 | file=self.file,
1291 | mode=mode.replace('_plus', '+'),
1292 | opener=opener
1293 | ),
1294 | buffer_size=bufsize
1295 | )
1296 | return buf if 'b' in mode else TextIOWrapper(
1297 | buffer =buf,
1298 | encoding =encoding,
1299 | errors =errors,
1300 | newline =newline,
1301 | line_buffering=line_buffering,
1302 | write_through =write_through
1303 | )
1304 |
1305 | init_buffer_instance.__name__ = mode
1306 | init_buffer_instance.__qualname__ = f'{Open.__name__}.{mode}'
1307 |
1308 | return init_buffer_instance
1309 |
1310 |
1311 | class Content(Open):
1312 |
1313 | def __dir__(self) -> Iterable[str]:
1314 | return object.__dir__(self)
1315 |
1316 | def __bytes__(self) -> bytes:
1317 | return self.rb().read()
1318 |
1319 | def __ior__(self, other: Union['Content', bytes], /) -> Self:
1320 | self.write(other)
1321 | return self
1322 |
1323 | def __iadd__(self, other: Union['Content', bytes], /) -> Self:
1324 | self.append(other)
1325 | return self
1326 |
1327 | def __contains__(self, subcontent: bytes, /) -> bool:
1328 | return self.contains(subcontent)
1329 |
1330 | def __eq__(self, other: Union['Content', bytes], /) -> bool:
1331 | if self is other:
1332 | return True
1333 |
1334 | if isinstance(other, Content):
1335 | if abspath(self.file) == abspath(other.file):
1336 | return True
1337 | read1, read2 = self.rb().read, other.rb().read
1338 | while True:
1339 | content1 = read1(READ_BUFSIZE)
1340 | content2 = read2(READ_BUFSIZE)
1341 | if content1 == content2 == b'':
1342 | return True
1343 | if content1 != content2:
1344 | return False
1345 |
1346 | elif other.__class__ is bytes:
1347 | start, end = 0, READ_BUFSIZE
1348 | read1 = self.rb().read
1349 | while True:
1350 | content1 = read1(READ_BUFSIZE)
1351 | if content1 == other[start:end] == b'':
1352 | return True
1353 | if content1 != other[start:end]:
1354 | return False
1355 | start += READ_BUFSIZE
1356 | end += READ_BUFSIZE
1357 |
1358 | raise TypeError(
1359 | 'content type to be equality judgment operation can only be '
1360 | f'"{__package__}.{Content.__name__}" or "bytes", '
1361 | f'not "{other.__class__.__name__}".'
1362 | )
1363 |
1364 | def __iter__(self) -> Iterator[bytes]:
1365 | return (line.rstrip(b'\r\n') for line in self.rb())
1366 |
1367 | def __len__(self) -> int:
1368 | return getsize(self.file)
1369 |
1370 | def __bool__(self) -> bool:
1371 | return bool(getsize(self.file))
1372 |
1373 | def read(self, size: int = -1, /) -> bytes:
1374 | return self.rb().read(size)
1375 |
1376 | def write(self, content: Union['Content', bytes], /) -> int:
1377 | if isinstance(content, Content):
1378 | if abspath(content.file) == abspath(self.file):
1379 | raise ex.IsSameFileError(
1380 | 'source and destination cannot be the same, '
1381 | f'path "{abspath(self.file)}".'
1382 | )
1383 | read, write, count = content.rb().read, self.wb().write, 0
1384 | while True:
1385 | content = read(READ_BUFSIZE)
1386 | if not content:
1387 | break
1388 | count += write(content)
1389 | # Beware of original data loss due to write failures (the `content` type
1390 | # error).
1391 | elif content.__class__ is bytes:
1392 | count = self.wb().write(content)
1393 | else:
1394 | raise TypeError(
1395 | 'content type to be written can only be '
1396 | f'"{__package__}.{Content.__name__}" or "bytes", '
1397 | f'not "{content.__class__.__name__}".'
1398 | )
1399 | return count
1400 |
1401 | def append(self, content: Union['Content', bytes], /) -> int:
1402 | if isinstance(content, Content):
1403 | read, write, count = content.rb().read, self.ab().write, 0
1404 | while True:
1405 | content = read(READ_BUFSIZE)
1406 | if not content:
1407 | break
1408 | count += write(content)
1409 | elif content.__class__ is bytes:
1410 | count = self.ab().write(content)
1411 | else:
1412 | raise TypeError(
1413 | 'content type to be appended can only be '
1414 | f'"{__package__}.{Content.__name__}" or "bytes", '
1415 | f'not "{content.__class__.__name__}".'
1416 | )
1417 | return count
1418 |
1419 | def contains(self, subcontent: bytes, /) -> bool:
1420 | if subcontent == b'':
1421 | return True
1422 |
1423 | deviation_index = -len(subcontent) + 1
1424 | deviation_value = b''
1425 |
1426 | read = self.rb().read
1427 |
1428 | while True:
1429 | content = read(READ_BUFSIZE)
1430 | if not content:
1431 | return False
1432 | if subcontent in deviation_value + content:
1433 | return True
1434 | deviation_value = content[deviation_index:]
1435 |
1436 | def copy(
1437 | self,
1438 | other: Union['Content', 'SupportsWrite[bytes]'],
1439 | /, *,
1440 | bufsize: int = READ_BUFSIZE
1441 | ) -> None:
1442 | write = (other.ab() if isinstance(other, Content) else other).write
1443 | read = self.rb().read
1444 |
1445 | while True:
1446 | content = read(bufsize)
1447 | if not content:
1448 | break
1449 | write(content)
1450 |
1451 | def truncate(self, length: int, /) -> None:
1452 | truncate(self.file, length)
1453 |
1454 | def clear(self) -> None:
1455 | truncate(self.file, 0)
1456 |
1457 | def md5(self, salting: bytes = b'') -> str:
1458 | md5 = hashlib.md5(salting)
1459 | read = self.rb().read
1460 |
1461 | while True:
1462 | content = read(READ_BUFSIZE)
1463 | if not content:
1464 | break
1465 | md5.update(content)
1466 |
1467 | return md5.hexdigest()
1468 |
1469 | overwrite = write
1470 |
1471 |
1472 | class tree:
1473 |
1474 | def __init__(
1475 | self,
1476 | dirpath: Optional[PathLink] = None,
1477 | /, *,
1478 | level: int = float('inf'),
1479 | downtop: Optional[bool] = None,
1480 | bottom_up: bool = UNIQUE,
1481 | omit_dir: bool = False,
1482 | pure_path: Optional[bool] = None,
1483 | mysophobia: bool = UNIQUE,
1484 | shortpath: bool = False
1485 | ):
1486 | if dirpath == b'':
1487 | dirpath: bytes = getcwdb()
1488 | elif dirpath in (None, ''):
1489 | dirpath: str = getcwd()
1490 |
1491 | self.root = dirpath
1492 |
1493 | if bottom_up is not UNIQUE:
1494 | warnings.warn(
1495 | 'parameter "bottom_up" will be deprecated soon, replaced to '
1496 | '"downtop".', stacklevel=2
1497 | )
1498 | if downtop is None:
1499 | downtop = bottom_up
1500 |
1501 | self.tree = (
1502 | self.downtop if downtop else self.topdown
1503 | )(dirpath, level=level)
1504 |
1505 | self.omit_dir = omit_dir
1506 |
1507 | if mysophobia is not UNIQUE:
1508 | warnings.warn(
1509 | 'parameter "mysophobia" will be deprecated soon, replaced to '
1510 | '"pure_path".', stacklevel=2
1511 | )
1512 | if pure_path is None:
1513 | pure_path = mysophobia
1514 |
1515 | self.pure_path = pure_path
1516 | self.shortpath = shortpath
1517 |
1518 | if self.pure_path and shortpath:
1519 | self.nullchar = b'' if dirpath.__class__ is bytes else ''
1520 |
1521 | def __iter__(self) -> Iterator[Union[Path, PathLink]]:
1522 | return self
1523 |
1524 | def __next__(self) -> Union[Path, PathLink]:
1525 | return next(self.tree)
1526 |
1527 | def topdown(
1528 | self, dirpath: PathLink, /, *, level: int
1529 | ) -> Iterator[Union[Path, PathLink]]:
1530 | for name in listdir(dirpath):
1531 | path: PathLink = join(dirpath, name)
1532 | is_dir: bool = isdir(path)
1533 | if not (is_dir and self.omit_dir):
1534 | yield self.path(path, is_dir=is_dir)
1535 | if level > 1 and is_dir:
1536 | yield from self.topdown(path, level=level - 1)
1537 |
1538 | def downtop(
1539 | self, dirpath: PathLink, /, *, level: int
1540 | ) -> Iterator[Union[Path, PathLink]]:
1541 | for name in listdir(dirpath):
1542 | path: PathLink = join(dirpath, name)
1543 | is_dir: bool = isdir(path)
1544 | if level > 1 and is_dir:
1545 | yield from self.downtop(path, level=level - 1)
1546 | if not (is_dir and self.omit_dir):
1547 | yield self.path(path, is_dir=is_dir)
1548 |
1549 | def path(
1550 | self, path: PathLink, /, *, is_dir: bool
1551 | ) -> Union[Path, PathLink]:
1552 | if self.pure_path:
1553 | return self.basepath(path) if self.shortpath else path
1554 | elif is_dir:
1555 | return Directory(path)
1556 | elif isfile(path):
1557 | return File(path)
1558 | else:
1559 | return Path(path)
1560 |
1561 | def basepath(self, path: PathLink, /) -> PathLink:
1562 | path: PathLink = path.replace(self.root, self.nullchar)
1563 | if path[0] in (47, 92, '/', '\\'):
1564 | path: PathLink = path[1:]
1565 | return path
1566 |
1567 |
1568 | class INI:
1569 |
1570 | def __init__(self, file: File, /):
1571 | self.file = file
1572 |
1573 | def read(
1574 | self,
1575 | *,
1576 | encoding: Optional[str] = None,
1577 | defaults: Optional[Mapping[str, str]] = None,
1578 | dict_type: Type[Mapping[str, str]] = dict,
1579 | allow_no_value: bool = False,
1580 | delimiters: Sequence[str] = ('=', ':'),
1581 | comment_prefixes: Sequence[str] = ('#', ';'),
1582 | inline_comment_prefixes: Optional[Sequence[str]] = None,
1583 | strict: bool = True,
1584 | empty_lines_in_values: bool = True,
1585 | default_section: str = 'DEFAULT',
1586 | interpolation: Optional['Interpolation'] = None,
1587 | converters: Optional[ConvertersMap] = None
1588 | ) -> ConfigParser:
1589 | kw = {}
1590 | if interpolation is not None:
1591 | kw['interpolation'] = interpolation
1592 | if converters is not None:
1593 | kw['converters'] = converters
1594 | config = ConfigParser(
1595 | defaults =defaults,
1596 | dict_type =dict_type,
1597 | allow_no_value =allow_no_value,
1598 | delimiters =delimiters,
1599 | comment_prefixes =comment_prefixes,
1600 | inline_comment_prefixes=inline_comment_prefixes,
1601 | strict =strict,
1602 | empty_lines_in_values =empty_lines_in_values,
1603 | default_section =default_section,
1604 | **kw
1605 | )
1606 | config.read(self.file, encoding=encoding)
1607 | return config
1608 |
1609 |
1610 | class CSV:
1611 |
1612 | def __init__(self, file: File, /):
1613 | self.file = file
1614 |
1615 | def reader(
1616 | self,
1617 | dialect: CSVDialectLike = 'excel',
1618 | *,
1619 | encoding: Optional[str] = None,
1620 | delimiter: str = ',',
1621 | quotechar: Optional[str] = '"',
1622 | escapechar: Optional[str] = None,
1623 | doublequote: bool = True,
1624 | skipinitialspace: bool = False,
1625 | lineterminator: str = '\r\n',
1626 | quoting: int = 0,
1627 | strict: bool = False
1628 | ) -> CSVReader:
1629 | return csv.reader(
1630 | Open(self.file).r(encoding=encoding, newline=''),
1631 | dialect,
1632 | delimiter =delimiter,
1633 | quotechar =quotechar,
1634 | escapechar =escapechar,
1635 | doublequote =doublequote,
1636 | skipinitialspace=skipinitialspace,
1637 | lineterminator =lineterminator,
1638 | quoting =quoting,
1639 | strict =strict
1640 | )
1641 |
1642 | def writer(
1643 | self,
1644 | dialect: CSVDialectLike = 'excel',
1645 | *,
1646 | mode: Literal['w', 'a'] = 'w',
1647 | encoding: Optional[str] = None,
1648 | delimiter: str = ',',
1649 | quotechar: Optional[str] = '"',
1650 | escapechar: Optional[str] = None,
1651 | doublequote: bool = True,
1652 | skipinitialspace: bool = False,
1653 | lineterminator: str = '\r\n',
1654 | quoting: int = 0,
1655 | strict: bool = False
1656 | ) -> CSVWriter:
1657 | if mode not in ('w', 'a'):
1658 | raise ex.ParameterError(
1659 | f'parameter "mode" must be "w" or "a", not {mode!r}.'
1660 | )
1661 | return csv.writer(
1662 | getattr(Open(self.file), mode)(encoding=encoding, newline=''),
1663 | dialect,
1664 | delimiter =delimiter,
1665 | quotechar =quotechar,
1666 | escapechar =escapechar,
1667 | doublequote =doublequote,
1668 | skipinitialspace=skipinitialspace,
1669 | lineterminator =lineterminator,
1670 | quoting =quoting,
1671 | strict =strict
1672 | )
1673 |
1674 |
1675 | class JSON:
1676 |
1677 | def __init__(self, file: File, /):
1678 | self.file = file
1679 |
1680 | def load(
1681 | self,
1682 | *,
1683 | cls: Type[json.JSONDecoder] = json.JSONDecoder,
1684 | object_hook: Optional[JsonObjectHook] = None,
1685 | parse_float: Optional[JsonObjectParse] = None,
1686 | parse_int: Optional[JsonObjectParse] = None,
1687 | parse_constant: Optional[JsonObjectParse] = None,
1688 | object_pairs_hook: Optional[JsonObjectPairsHook] = None
1689 | ) -> Any:
1690 | return json.loads(
1691 | self.file.content,
1692 | cls =cls,
1693 | object_hook =object_hook,
1694 | parse_float =parse_float,
1695 | parse_int =parse_int,
1696 | parse_constant =parse_constant,
1697 | object_pairs_hook=object_pairs_hook
1698 | )
1699 |
1700 | def dump(
1701 | self,
1702 | obj: Any,
1703 | *,
1704 | encoding: Optional[str] = None,
1705 | skipkeys: bool = False,
1706 | ensure_ascii: bool = True,
1707 | check_circular: bool = True,
1708 | allow_nan: bool = True,
1709 | cls: Type[json.JSONEncoder] = json.JSONEncoder,
1710 | indent: Optional[Union[int, str]] = None,
1711 | separators: Optional[Tuple[str, str]] = None,
1712 | default: Optional[Callable[[Any], Any]] = None,
1713 | sort_keys: bool = False,
1714 | **kw
1715 | ) -> None:
1716 | return json.dump(
1717 | obj,
1718 | Open(self.file).w(encoding=encoding),
1719 | skipkeys =skipkeys,
1720 | ensure_ascii =ensure_ascii,
1721 | check_circular=check_circular,
1722 | allow_nan =allow_nan,
1723 | cls =cls,
1724 | indent =indent,
1725 | separators =separators,
1726 | default =default,
1727 | sort_keys =sort_keys,
1728 | **kw
1729 | )
1730 |
1731 |
1732 | class YAML:
1733 |
1734 | def __init__(self, file: File, /):
1735 | if yaml is None:
1736 | raise ModuleNotFoundError(
1737 | 'dependency has not been installed, '
1738 | 'run `pip3 install systempath[pyyaml]`.'
1739 | )
1740 | self.file = file
1741 |
1742 | def load(self, loader: Optional['YamlLoader'] = None) -> Any:
1743 | return yaml.load(FileIO(self.file), loader or yaml.SafeLoader)
1744 |
1745 | def load_all(self, loader: Optional['YamlLoader'] = None) -> Iterator[Any]:
1746 | return yaml.load_all(FileIO(self.file), loader or yaml.SafeLoader)
1747 |
1748 | def dump(
1749 | self,
1750 | data: Any,
1751 | /,
1752 | dumper: Optional['YamlDumper'] = None,
1753 | *,
1754 | default_style: Optional[str] = None,
1755 | default_flow_style: bool = False,
1756 | canonical: Optional[bool] = None,
1757 | indent: Optional[int] = None,
1758 | width: Optional[int] = None,
1759 | allow_unicode: Optional[bool] = None,
1760 | line_break: Optional[str] = None,
1761 | encoding: Optional[str] = None,
1762 | explicit_start: Optional[bool] = None,
1763 | explicit_end: Optional[bool] = None,
1764 | version: Optional[Tuple[int, int]] = None,
1765 | tags: Optional[Mapping[str, str]] = None,
1766 | sort_keys: bool = True
1767 | ) -> None:
1768 | return yaml.dump_all(
1769 | [data],
1770 | Open(self.file).w(encoding=encoding),
1771 | dumper or yaml.Dumper,
1772 | default_style =default_style,
1773 | default_flow_style=default_flow_style,
1774 | canonical =canonical,
1775 | indent =indent,
1776 | width =width,
1777 | allow_unicode =allow_unicode,
1778 | line_break =line_break,
1779 | encoding =encoding,
1780 | explicit_start =explicit_start,
1781 | explicit_end =explicit_end,
1782 | version =version,
1783 | tags =tags,
1784 | sort_keys =sort_keys
1785 | )
1786 |
1787 | def dump_all(
1788 | self,
1789 | documents: Iterable[Any],
1790 | /,
1791 | dumper: Optional['YamlLoader'] = None,
1792 | *,
1793 | default_style: Optional[YamlDumpStyle] = None,
1794 | default_flow_style: bool = False,
1795 | canonical: Optional[bool] = None,
1796 | indent: Optional[int] = None,
1797 | width: Optional[int] = None,
1798 | allow_unicode: Optional[bool] = None,
1799 | line_break: Optional[FileNewline] = None,
1800 | encoding: Optional[str] = None,
1801 | explicit_start: Optional[bool] = None,
1802 | explicit_end: Optional[bool] = None,
1803 | version: Optional[Tuple[int, int]] = None,
1804 | tags: Optional[Mapping[str, str]] = None,
1805 | sort_keys: bool = True
1806 | ) -> None:
1807 | return yaml.dump_all(
1808 | documents,
1809 | Open(self.file).w(encoding=encoding),
1810 | dumper or yaml.Dumper,
1811 | default_style =default_style,
1812 | default_flow_style=default_flow_style,
1813 | canonical =canonical,
1814 | indent =indent,
1815 | width =width,
1816 | allow_unicode =allow_unicode,
1817 | line_break =line_break,
1818 | encoding =encoding,
1819 | explicit_start =explicit_start,
1820 | explicit_end =explicit_end,
1821 | version =version,
1822 | tags =tags,
1823 | sort_keys =sort_keys
1824 | )
1825 |
1826 |
1827 | class SystemPath(Directory, File):
1828 |
1829 | def __init__(
1830 | self,
1831 | root: PathLink = '.',
1832 | /, *,
1833 | autoabs: bool = False,
1834 | strict: bool = False,
1835 | dir_fd: Optional[int] = None,
1836 | follow_symlinks: bool = True
1837 | ):
1838 | Path.__init__(
1839 | self,
1840 | '.' if root == '' else b'.' if root == b'' else root,
1841 | autoabs =autoabs,
1842 | strict =strict,
1843 | dir_fd =dir_fd,
1844 | follow_symlinks=follow_symlinks
1845 | )
1846 |
1847 | __new__ = Path.__new__
1848 | __bool__ = Path.__bool__
1849 | __truediv__ = Path.__truediv__
1850 |
1851 | isempty = Path.isempty
1852 |
--------------------------------------------------------------------------------