├── .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 | [LOGO](http://gqylpy.com) 2 | [![Release](https://img.shields.io/github/release/gqylpy/systempath.svg?style=flat-square)](https://github.com/gqylpy/systempath/releases/latest) 3 | [![Python Versions](https://img.shields.io/pypi/pyversions/systempath)](https://pypi.org/project/systempath) 4 | [![License](https://img.shields.io/pypi/l/systempath)](https://github.com/gqylpy/systempath/blob/master/LICENSE) 5 | [![Downloads](https://static.pepy.tech/badge/systempath)](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 | [LOGO](http://gqylpy.com) 2 | [![Release](https://img.shields.io/github/release/gqylpy/systempath.svg?style=flat-square")](https://github.com/gqylpy/systempath/releases/latest) 3 | [![Python Versions](https://img.shields.io/pypi/pyversions/systempath)](https://pypi.org/project/systempath) 4 | [![License](https://img.shields.io/pypi/l/systempath)](https://github.com/gqylpy/systempath/blob/master/LICENSE) 5 | [![Downloads](https://static.pepy.tech/badge/systempath)](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 | --------------------------------------------------------------------------------