tretool 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tretool/path.py ADDED
@@ -0,0 +1,1139 @@
1
+ import os
2
+ import sys
3
+ import stat
4
+ import shutil
5
+ import fnmatch
6
+ import tempfile
7
+ import hashlib
8
+ import filecmp
9
+ from abc import ABC, abstractmethod
10
+ from datetime import datetime
11
+ from typing import (
12
+ Union, List, Optional, Iterator, Tuple, Dict, Any, overload, IO
13
+ )
14
+
15
+ PathLike = Union[str, bytes, os.PathLike]
16
+
17
+ class PurePath(ABC):
18
+ """
19
+ 纯路径基类,提供与操作系统无关的路径操作
20
+
21
+ 这是一个抽象基类,提供不依赖实际文件系统的路径操作。
22
+ 子类应实现特定平台的行为。
23
+
24
+ 参数:
25
+ *pathsegments: 路径组成部分,可以是字符串、字节或os.PathLike对象
26
+
27
+ 示例:
28
+ >>> path = PurePath('foo', 'bar', 'baz.txt')
29
+ >>> str(path)
30
+ 'foo/bar/baz.txt'
31
+ """
32
+
33
+ def __init__(self, *pathsegments: PathLike):
34
+ """初始化一个新的PurePath实例"""
35
+ self._parts = self._parse_args(pathsegments)
36
+ self._drv = self._parse_drive(self._parts[0]) if self._parts else ''
37
+ self._root = self._parse_root(self._parts[0]) if self._parts else ''
38
+
39
+ def _parse_args(self, args) -> List[str]:
40
+ """
41
+ 将路径参数解析为路径组件列表
42
+
43
+ 参数:
44
+ args: 路径段序列
45
+
46
+ 返回:
47
+ 清理后的路径组件列表
48
+
49
+ 异常:
50
+ TypeError: 如果参数不是字符串、字节或os.PathLike对象
51
+ """
52
+ parts = []
53
+ for arg in args:
54
+ if isinstance(arg, (str, bytes)):
55
+ if isinstance(arg, bytes):
56
+ arg = arg.decode('utf-8', 'surrogateescape')
57
+ parts.extend(arg.split(os.sep))
58
+ elif hasattr(arg, '__fspath__'):
59
+ parts.extend(str(arg).split(os.sep))
60
+ else:
61
+ raise TypeError(f"参数应为路径或字符串,不是 {type(arg)}")
62
+ return [p for p in parts if p] # 过滤空部分
63
+
64
+ def _parse_drive(self, part: str) -> str:
65
+ """
66
+ 从路径组件中提取驱动器号(Windows专用)
67
+
68
+ 参数:
69
+ part: 第一个路径组件
70
+
71
+ 返回:
72
+ 驱动器号(如'C:')或空字符串
73
+ """
74
+ if len(part) > 1 and part[1] == ':':
75
+ return part[:2]
76
+ return ''
77
+
78
+ def _parse_root(self, part: str) -> str:
79
+ """
80
+ 从路径组件中提取根目录
81
+
82
+ 参数:
83
+ part: 第一个路径组件
84
+
85
+ 返回:
86
+ 根目录(如'/')或空字符串
87
+ """
88
+ if part.startswith(os.sep):
89
+ return os.sep
90
+ return ''
91
+
92
+ @property
93
+ def parts(self) -> Tuple[str, ...]:
94
+ """
95
+ 以元组形式访问路径的各个组件
96
+
97
+ 返回:
98
+ 路径组件的元组
99
+ """
100
+ return tuple(self._parts)
101
+
102
+ @property
103
+ def drive(self) -> str:
104
+ """
105
+ 驱动器字母或名称(仅Windows)
106
+
107
+ 返回:
108
+ 驱动器号(如'C:')或空字符串
109
+ """
110
+ return self._drv
111
+
112
+ @property
113
+ def root(self) -> str:
114
+ """
115
+ 路径的根目录(如果有)
116
+
117
+ 返回:
118
+ 根目录(如'/')或空字符串
119
+ """
120
+ return self._root
121
+
122
+ @property
123
+ def name(self) -> str:
124
+ """
125
+ 路径的最后一个组件(如果有)
126
+
127
+ 返回:
128
+ 最后一个路径组件或空字符串
129
+ """
130
+ if not self._parts:
131
+ return ''
132
+ return self._parts[-1]
133
+
134
+ @property
135
+ def suffix(self) -> str:
136
+ """
137
+ 最后一个组件的文件扩展名
138
+
139
+ 返回:
140
+ 包含点的文件扩展名(如'.txt')或空字符串
141
+ """
142
+ name = self.name
143
+ i = name.rfind('.')
144
+ if 0 < i < len(name) - 1:
145
+ return name[i:]
146
+ return ''
147
+
148
+ @property
149
+ def suffixes(self) -> List[str]:
150
+ """
151
+ 路径的所有文件扩展名列表
152
+
153
+ 返回:
154
+ 包含点的扩展名列表(如['.tar', '.gz'])
155
+ """
156
+ name = self.name
157
+ if name.endswith('.'):
158
+ return []
159
+ return ['.' + ext for ext in name.split('.')[1:]]
160
+
161
+ @property
162
+ def stem(self) -> str:
163
+ """
164
+ 最后一个组件不带后缀
165
+
166
+ 返回:
167
+ 不带扩展名的名称
168
+ """
169
+ name = self.name
170
+ i = name.rfind('.')
171
+ if 0 < i < len(name) - 1:
172
+ return name[:i]
173
+ return name
174
+
175
+ def joinpath(self, *args) -> 'PurePath':
176
+ """
177
+ 将此路径与一个或多个参数组合
178
+
179
+ 参数:
180
+ *args: 要连接的路径段
181
+
182
+ 返回:
183
+ 新路径,组合了当前路径和参数
184
+ """
185
+ return self.__class__(self, *args)
186
+
187
+ def __truediv__(self, other) -> 'PurePath':
188
+ """
189
+ 使用/运算符连接路径
190
+
191
+ 参数:
192
+ other: 要追加的路径段
193
+
194
+ 返回:
195
+ 新组合的路径
196
+ """
197
+ return self.joinpath(other)
198
+
199
+ def __str__(self) -> str:
200
+ """返回路径的字符串表示"""
201
+ if self._drv and self._root:
202
+ return self._drv + self._root + os.sep.join(self._parts[1:])
203
+ elif self._root:
204
+ return self._root + os.sep.join(self._parts)
205
+ elif self._drv:
206
+ return self._drv + os.sep.join(self._parts)
207
+ return os.sep.join(self._parts)
208
+
209
+ def __repr__(self) -> str:
210
+ """返回路径的正式字符串表示"""
211
+ return f"{self.__class__.__name__}('{self}')"
212
+
213
+ def __eq__(self, other) -> bool:
214
+ """比较路径是否相等"""
215
+ if not isinstance(other, PurePath):
216
+ return NotImplemented
217
+ return str(self) == str(other)
218
+
219
+ def __hash__(self) -> int:
220
+ """计算路径的哈希值"""
221
+ return hash(str(self))
222
+
223
+
224
+ class PurePosixPath(PurePath):
225
+ """
226
+ 非Windows系统的PurePath子类
227
+
228
+ 此版本不支持驱动器号并使用正斜杠
229
+ """
230
+
231
+
232
+ class PureWindowsPath(PurePath):
233
+ """
234
+ Windows系统的PurePath子类
235
+
236
+ 此版本支持驱动器号并使用反斜杠
237
+ """
238
+
239
+
240
+ class Path(PurePath):
241
+ """
242
+ 具体路径类,提供文件系统操作
243
+
244
+ 此类继承自PurePath并添加实际文件系统操作
245
+ """
246
+
247
+ def stat(self) -> os.stat_result:
248
+ """
249
+ 返回此路径的stat信息
250
+
251
+ 返回:
252
+ os.stat_result对象
253
+
254
+ 异常:
255
+ OSError: 如果路径不存在
256
+ """
257
+ return os.stat(str(self))
258
+
259
+ def exists(self) -> bool:
260
+ """
261
+ 检查路径是否存在
262
+
263
+ 返回:
264
+ 如果路径存在返回True,否则返回False
265
+ """
266
+ return os.path.exists(str(self))
267
+
268
+ def is_file(self) -> bool:
269
+ """
270
+ 检查路径是否是普通文件
271
+
272
+ 返回:
273
+ 如果路径是文件返回True,否则返回False
274
+ """
275
+ return os.path.isfile(str(self))
276
+
277
+ def is_dir(self) -> bool:
278
+ """
279
+ 检查路径是否是目录
280
+
281
+ 返回:
282
+ 如果路径是目录返回True,否则返回False
283
+ """
284
+ return os.path.isdir(str(self))
285
+
286
+ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
287
+ """
288
+ 创建新目录
289
+
290
+ 参数:
291
+ mode: 权限(八进制数)
292
+ parents: 如果需要是否创建父目录
293
+ exist_ok: 如果目录已存在是否不报错
294
+
295
+ 异常:
296
+ FileExistsError: 如果目录已存在且exist_ok为False
297
+ OSError: 如果目录无法创建
298
+ """
299
+ path = str(self)
300
+ if parents:
301
+ os.makedirs(path, mode, exist_ok=exist_ok)
302
+ else:
303
+ try:
304
+ os.mkdir(path, mode)
305
+ except FileExistsError:
306
+ if not exist_ok:
307
+ raise
308
+
309
+ def open(self, mode='r', buffering=-1, encoding=None,
310
+ errors=None, newline=None) -> IO:
311
+ """
312
+ 打开此路径指向的文件
313
+
314
+ 参数:
315
+ mode: 打开模式('r', 'w'等)
316
+ buffering: 缓冲策略
317
+ encoding: 文本编码
318
+ errors: 编码错误处理策略
319
+ newline: 换行控制
320
+
321
+ 返回:
322
+ 文件对象
323
+
324
+ 异常:
325
+ OSError: 如果文件无法打开
326
+ """
327
+ return open(str(self), mode, buffering, encoding, errors, newline)
328
+
329
+ def read_text(self, encoding=None, errors=None) -> str:
330
+ """
331
+ 以文本形式读取文件内容
332
+
333
+ 参数:
334
+ encoding: 文本编码
335
+ errors: 编码错误处理策略
336
+
337
+ 返回:
338
+ 文件内容字符串
339
+
340
+ 异常:
341
+ OSError: 如果文件无法读取
342
+ """
343
+ with open(str(self), 'r', encoding=encoding, errors=errors) as f:
344
+ return f.read()
345
+
346
+ def write_text(self, data: str, encoding=None, errors=None):
347
+ """
348
+ 以文本形式写入文件内容
349
+
350
+ 参数:
351
+ data: 要写入的字符串
352
+ encoding: 文本编码
353
+ errors: 编码错误处理策略
354
+
355
+ 返回:
356
+ 写入的字符数
357
+
358
+ 异常:
359
+ OSError: 如果文件无法写入
360
+ """
361
+ with open(str(self), 'w', encoding=encoding, errors=errors) as f:
362
+ return f.write(data)
363
+
364
+ def read_bytes(self) -> bytes:
365
+ """
366
+ 以二进制形式读取文件内容
367
+
368
+ 返回:
369
+ 文件内容的字节串
370
+
371
+ 异常:
372
+ OSError: 如果文件无法读取
373
+ """
374
+ with open(str(self), 'rb') as f:
375
+ return f.read()
376
+
377
+ def write_bytes(self, data: bytes):
378
+ """
379
+ 以二进制形式写入文件内容
380
+
381
+ 参数:
382
+ data: 要写入的字节串
383
+
384
+ 返回:
385
+ 写入的字节数
386
+
387
+ 异常:
388
+ OSError: 如果文件无法写入
389
+ """
390
+ with open(str(self), 'wb') as f:
391
+ return f.write(data)
392
+
393
+ def unlink(self, missing_ok=False):
394
+ """
395
+ 删除文件
396
+
397
+ 参数:
398
+ missing_ok: 如果文件不存在是否不报错
399
+
400
+ 异常:
401
+ FileNotFoundError: 如果文件不存在且missing_ok为False
402
+ OSError: 如果文件无法删除
403
+ """
404
+ try:
405
+ os.unlink(str(self))
406
+ except FileNotFoundError:
407
+ if not missing_ok:
408
+ raise
409
+
410
+ def rmdir(self):
411
+ """
412
+ 删除空目录
413
+
414
+ 异常:
415
+ OSError: 如果目录不为空或无法删除
416
+ """
417
+ os.rmdir(str(self))
418
+
419
+ def rename(self, target) -> 'Path':
420
+ """
421
+ 重命名文件或目录
422
+
423
+ 参数:
424
+ target: 目标路径
425
+
426
+ 返回:
427
+ 新的Path对象
428
+
429
+ 异常:
430
+ OSError: 如果重命名失败
431
+ """
432
+ os.rename(str(self), str(target))
433
+ return self.__class__(target)
434
+
435
+ def resolve(self) -> 'Path':
436
+ """
437
+ 解析符号链接的绝对路径
438
+
439
+ 返回:
440
+ 解析后的新Path对象
441
+ """
442
+ return self.__class__(os.path.realpath(str(self)))
443
+
444
+ def absolute(self) -> 'Path':
445
+ """
446
+ 获取绝对路径
447
+
448
+ 返回:
449
+ 绝对路径的新Path对象
450
+ """
451
+ return self.__class__(os.path.abspath(str(self)))
452
+
453
+ def glob(self, pattern: str) -> Iterator['Path']:
454
+ """
455
+ 匹配文件模式
456
+
457
+ 参数:
458
+ pattern: 要匹配的模式(如'*.txt')
459
+
460
+ 返回:
461
+ 匹配路径的生成器
462
+ """
463
+ import glob
464
+ for p in glob.glob(str(self / pattern)):
465
+ yield self.__class__(p)
466
+
467
+ def rglob(self, pattern: str) -> Iterator['Path']:
468
+ """
469
+ 递归匹配文件模式
470
+
471
+ 参数:
472
+ pattern: 要匹配的模式
473
+
474
+ 返回:
475
+ 匹配路径的生成器
476
+ """
477
+ return self.glob('**' + os.sep + pattern)
478
+
479
+ def iterdir(self) -> Iterator['Path']:
480
+ """
481
+ 迭代目录内容
482
+
483
+ 返回:
484
+ 目录中子项的Path对象生成器
485
+
486
+ 异常:
487
+ OSError: 如果不是目录或无法读取
488
+ """
489
+ for name in os.listdir(str(self)):
490
+ yield self.__class__(self, name)
491
+
492
+ @property
493
+ def parent(self) -> 'Path':
494
+ """
495
+ 父目录
496
+
497
+ 返回:
498
+ 父目录的Path对象
499
+ """
500
+ return self.__class__(os.path.dirname(str(self)))
501
+
502
+ @property
503
+ def parents(self) -> List['Path']:
504
+ """
505
+ 所有父目录列表
506
+
507
+ 返回:
508
+ 从近到远的所有父目录Path对象列表
509
+ """
510
+ path = self
511
+ parents = []
512
+ while True:
513
+ parent = path.parent
514
+ if parent == path:
515
+ break
516
+ parents.append(parent)
517
+ path = parent
518
+ return parents
519
+
520
+ def with_name(self, name: str) -> 'Path':
521
+ """
522
+ 返回修改文件名后的新路径
523
+
524
+ 参数:
525
+ name: 新文件名
526
+
527
+ 返回:
528
+ 新Path对象
529
+
530
+ 异常:
531
+ ValueError: 如果当前路径没有名称
532
+ """
533
+ if not self.name:
534
+ raise ValueError(f"{self} 没有名称")
535
+ return self.__class__(self.parent, name)
536
+
537
+ def with_suffix(self, suffix: str) -> 'Path':
538
+ """
539
+ 返回修改后缀后的新路径
540
+
541
+ 参数:
542
+ suffix: 新后缀(必须以点开头)
543
+
544
+ 返回:
545
+ 新Path对象
546
+
547
+ 异常:
548
+ ValueError: 如果后缀无效或路径没有名称
549
+ """
550
+ if not suffix.startswith('.'):
551
+ raise ValueError("无效的后缀")
552
+ name = self.name
553
+ if not name:
554
+ raise ValueError(f"{self} 没有名称")
555
+
556
+ old_suffix = self.suffix
557
+ if old_suffix:
558
+ name = name[:-len(old_suffix)]
559
+ return self.__class__(self.parent, name + suffix)
560
+
561
+ # ===== 增强功能 =====
562
+ def touch(self, mode=0o666, exist_ok=True):
563
+ """
564
+ 创建空文件(类似Unix touch命令)
565
+
566
+ 参数:
567
+ mode: 文件权限
568
+ exist_ok: 如果文件存在是否不报错
569
+
570
+ 异常:
571
+ FileExistsError: 如果文件存在且exist_ok为False
572
+ OSError: 如果文件无法创建
573
+ """
574
+ if self.exists():
575
+ if exist_ok:
576
+ os.utime(str(self), None)
577
+ else:
578
+ raise FileExistsError(str(self))
579
+ else:
580
+ open(str(self), 'a').close()
581
+ self.chmod(mode)
582
+
583
+ def copy(self, target, follow_symlinks=True):
584
+ """
585
+ 复制文件到目标路径
586
+
587
+ 参数:
588
+ target: 目标路径
589
+ follow_symlinks: 是否跟随符号链接
590
+
591
+ 返回:
592
+ 目标路径的Path对象
593
+
594
+ 异常:
595
+ OSError: 如果复制失败
596
+ """
597
+ shutil.copy2(str(self), str(target), follow_symlinks=follow_symlinks)
598
+ return self.__class__(target)
599
+
600
+ def move(self, target):
601
+ """
602
+ 移动文件/目录到目标位置
603
+
604
+ 参数:
605
+ target: 目标路径
606
+
607
+ 返回:
608
+ 目标路径的Path对象
609
+
610
+ 异常:
611
+ OSError: 如果移动失败
612
+ """
613
+ shutil.move(str(self), str(target))
614
+ return self.__class__(target)
615
+
616
+ def size(self) -> int:
617
+ """
618
+ 获取文件大小(字节)
619
+
620
+ 返回:
621
+ 文件大小(字节)
622
+
623
+ 异常:
624
+ OSError: 如果无法获取大小
625
+ """
626
+ return self.stat().st_size
627
+
628
+ def access_time(self) -> datetime:
629
+ """
630
+ 获取最后访问时间
631
+
632
+ 返回:
633
+ 最后访问时间的datetime对象
634
+ """
635
+ return datetime.fromtimestamp(self.stat().st_atime)
636
+
637
+ def modify_time(self) -> datetime:
638
+ """
639
+ 获取最后修改时间
640
+
641
+ 返回:
642
+ 最后修改时间的datetime对象
643
+ """
644
+ return datetime.fromtimestamp(self.stat().st_mtime)
645
+
646
+ def create_time(self) -> datetime:
647
+ """
648
+ 获取创建时间(Windows)或元数据修改时间(Unix)
649
+
650
+ 返回:
651
+ 创建时间的datetime对象
652
+ """
653
+ if os.name == 'nt':
654
+ return datetime.fromtimestamp(self.stat().st_ctime)
655
+ return datetime.fromtimestamp(self.stat().st_mtime)
656
+
657
+ def rmtree(self, ignore_errors=False, onerror=None):
658
+ """
659
+ 递归删除目录树
660
+
661
+ 参数:
662
+ ignore_errors: 是否忽略错误
663
+ onerror: 错误处理回调函数
664
+
665
+ 异常:
666
+ OSError: 如果删除失败且ignore_errors为False
667
+ """
668
+ shutil.rmtree(str(self), ignore_errors, onerror)
669
+
670
+ def find(self, pattern: str = "*") -> Iterator['Path']:
671
+ """
672
+ 递归查找匹配模式的文件
673
+
674
+ 参数:
675
+ pattern: 要匹配的模式(如'*.py')
676
+
677
+ 返回:
678
+ 匹配路径的生成器
679
+ """
680
+ for root, _, files in os.walk(str(self)):
681
+ for name in fnmatch.filter(files, pattern):
682
+ yield self.__class__(root, name)
683
+
684
+ def read_lines(self, encoding=None, errors=None) -> List[str]:
685
+ """
686
+ 按行读取文件内容
687
+
688
+ 参数:
689
+ encoding: 文本编码
690
+ errors: 编码错误处理策略
691
+
692
+ 返回:
693
+ 行列表
694
+
695
+ 异常:
696
+ OSError: 如果文件无法读取
697
+ """
698
+ with self.open('r', encoding=encoding, errors=errors) as f:
699
+ return f.readlines()
700
+
701
+ def write_lines(self, lines: List[str], encoding=None, errors=None):
702
+ """
703
+ 按行写入文件内容
704
+
705
+ 参数:
706
+ lines: 要写入的行列表
707
+ encoding: 文本编码
708
+ errors: 编码错误处理策略
709
+
710
+ 异常:
711
+ OSError: 如果文件无法写入
712
+ """
713
+ with self.open('w', encoding=encoding, errors=errors) as f:
714
+ f.writelines(lines)
715
+
716
+ def append_text(self, text: str, encoding=None, errors=None):
717
+ """
718
+ 追加文本内容
719
+
720
+ 参数:
721
+ text: 要追加的文本
722
+ encoding: 文本编码
723
+ errors: 编码错误处理策略
724
+
725
+ 异常:
726
+ OSError: 如果文件无法写入
727
+ """
728
+ with self.open('a', encoding=encoding, errors=errors) as f:
729
+ f.write(text)
730
+
731
+ def md5(self, chunk_size=8192) -> str:
732
+ """
733
+ 计算文件的MD5哈希值
734
+
735
+ 参数:
736
+ chunk_size: 读取块大小
737
+
738
+ 返回:
739
+ MD5哈希字符串
740
+ """
741
+ hash_md5 = hashlib.md5()
742
+ with self.open('rb') as f:
743
+ for chunk in iter(lambda: f.read(chunk_size), b""):
744
+ hash_md5.update(chunk)
745
+ return hash_md5.hexdigest()
746
+
747
+ def sha256(self, chunk_size=8192) -> str:
748
+ """
749
+ 计算文件的SHA256哈希值
750
+
751
+ 参数:
752
+ chunk_size: 读取块大小
753
+
754
+ 返回:
755
+ SHA256哈希字符串
756
+ """
757
+ hash_sha = hashlib.sha256()
758
+ with self.open('rb') as f:
759
+ for chunk in iter(lambda: f.read(chunk_size), b""):
760
+ hash_sha.update(chunk)
761
+ return hash_sha.hexdigest()
762
+
763
+ def compare(self, other: 'Path', shallow=True) -> bool:
764
+ """
765
+ 比较文件内容是否相同
766
+
767
+ 参数:
768
+ other: 要比较的另一路径
769
+ shallow: 是否只比较元数据
770
+
771
+ 返回:
772
+ 如果内容相同返回True,否则返回False
773
+ """
774
+ if not isinstance(other, Path):
775
+ other = Path(other)
776
+ return filecmp.cmp(str(self), str(other), shallow=shallow)
777
+
778
+ @classmethod
779
+ def temp_file(cls, suffix=None, prefix=None, dir=None) -> 'Path':
780
+ """
781
+ 创建临时文件
782
+
783
+ 参数:
784
+ suffix: 后缀名
785
+ prefix: 前缀名
786
+ dir: 目录路径
787
+
788
+ 返回:
789
+ 临时文件的Path对象
790
+ """
791
+ fd, name = tempfile.mkstemp(suffix, prefix, dir)
792
+ os.close(fd)
793
+ return cls(name)
794
+
795
+ @classmethod
796
+ def temp_dir(cls, suffix=None, prefix=None, dir=None) -> 'Path':
797
+ """
798
+ 创建临时目录
799
+
800
+ 参数:
801
+ suffix: 后缀名
802
+ prefix: 前缀名
803
+ dir: 目录路径
804
+
805
+ 返回:
806
+ 临时目录的Path对象
807
+ """
808
+ return cls(tempfile.mkdtemp(suffix, prefix, dir))
809
+
810
+ def readlink(self) -> 'Path':
811
+ """
812
+ 读取符号链接指向的实际路径
813
+
814
+ 返回:
815
+ 实际路径的Path对象
816
+
817
+ 异常:
818
+ OSError: 如果不是符号链接或无法读取
819
+ """
820
+ if not self.is_symlink():
821
+ raise OSError(f"{self} 不是符号链接")
822
+ return self.__class__(os.readlink(str(self)))
823
+
824
+ def chmod(self, mode: int, *, follow_symlinks=True):
825
+ """
826
+ 修改文件权限
827
+
828
+ 参数:
829
+ mode: 权限模式(八进制)
830
+ follow_symlinks: 是否跟随符号链接
831
+
832
+ 异常:
833
+ OSError: 如果修改失败
834
+ """
835
+ if follow_symlinks:
836
+ os.chmod(str(self), mode)
837
+ else:
838
+ if hasattr(os, 'lchmod'): # 仅Unix
839
+ os.lchmod(str(self), mode)
840
+
841
+ def is_executable(self) -> bool:
842
+ """
843
+ 检查文件是否可执行
844
+
845
+ 返回:
846
+ 如果可执行返回True,否则返回False
847
+ """
848
+ return os.access(str(self), os.X_OK)
849
+
850
+ def relative_to(self, other: Union['Path', str]) -> 'Path':
851
+ """
852
+ 计算相对于另一路径的相对路径
853
+
854
+ 参数:
855
+ other: 基准路径
856
+
857
+ 返回:
858
+ 相对路径的Path对象
859
+
860
+ 异常:
861
+ ValueError: 如果路径没有共同前缀
862
+ """
863
+ if not isinstance(other, Path):
864
+ other = Path(other)
865
+ return self.__class__(os.path.relpath(str(self), str(other)))
866
+
867
+ def matches(self, pattern: str) -> bool:
868
+ """
869
+ 检查路径是否匹配通配符模式
870
+
871
+ 参数:
872
+ pattern: 要匹配的模式
873
+
874
+ 返回:
875
+ 如果匹配返回True,否则返回False
876
+ """
877
+ return fnmatch.fnmatch(str(self), pattern)
878
+
879
+ def __enter__(self):
880
+ """支持with语句,返回自身"""
881
+ return self
882
+
883
+ def __exit__(self, exc_type, exc_val, exc_tb):
884
+ """支持with语句,无操作"""
885
+ pass
886
+
887
+
888
+ class PosixPath(Path, PurePosixPath):
889
+ """
890
+ POSIX系统专用路径实现
891
+
892
+ 仅在POSIX兼容系统(如Linux, macOS)上可用
893
+ """
894
+
895
+ def __new__(cls, *args, **kwargs):
896
+ if os.name != 'posix' and os.name != 'java' and not sys.platform.startswith('linux'):
897
+ raise NotImplementedError("PosixPath 仅支持POSIX系统")
898
+ return super().__new__(cls, *args, **kwargs)
899
+
900
+ def resolve(self, strict=False) -> 'PosixPath':
901
+ """
902
+ 解析符号链接的绝对路径
903
+
904
+ 参数:
905
+ strict: 如果路径不存在是否抛出异常
906
+
907
+ 返回:
908
+ 解析后的新Path对象
909
+
910
+ 异常:
911
+ FileNotFoundError: 如果路径不存在且strict为True
912
+ """
913
+ path = os.path.realpath(str(self))
914
+ if strict and not os.path.exists(path):
915
+ raise FileNotFoundError(path)
916
+ return self.__class__(path)
917
+
918
+ def owner(self) -> str:
919
+ """
920
+ 获取文件所有者
921
+
922
+ 返回:
923
+ 所有者用户名
924
+
925
+ 异常:
926
+ OSError: 如果无法获取所有者信息
927
+ """
928
+ import pwd
929
+ return pwd.getpwuid(self.stat().st_uid).pw_name
930
+
931
+ def group(self) -> str:
932
+ """
933
+ 获取文件所属组
934
+
935
+ 返回:
936
+ 组名
937
+
938
+ 异常:
939
+ OSError: 如果无法获取组信息
940
+ """
941
+ import grp
942
+ return grp.getgrgid(self.stat().st_gid).gr_name
943
+
944
+ def symlink_to(self, target, target_is_directory=False):
945
+ """
946
+ 创建符号链接
947
+
948
+ 参数:
949
+ target: 目标路径
950
+ target_is_directory: 目标是否是目录
951
+
952
+ 异常:
953
+ OSError: 如果创建失败
954
+ """
955
+ os.symlink(
956
+ str(target),
957
+ str(self),
958
+ target_is_directory=target_is_directory
959
+ )
960
+
961
+ def is_symlink(self) -> bool:
962
+ """
963
+ 检查是否是符号链接
964
+
965
+ 返回:
966
+ 如果是符号链接返回True,否则返回False
967
+ """
968
+ return os.path.islink(str(self))
969
+
970
+ def is_socket(self) -> bool:
971
+ """
972
+ 检查是否是套接字文件
973
+
974
+ 返回:
975
+ 如果是套接字文件返回True,否则返回False
976
+ """
977
+ return stat.S_ISSOCK(self.stat().st_mode)
978
+
979
+ def is_fifo(self) -> bool:
980
+ """
981
+ 检查是否是FIFO管道
982
+
983
+ 返回:
984
+ 如果是FIFO管道返回True,否则返回False
985
+ """
986
+ return stat.S_ISFIFO(self.stat().st_mode)
987
+
988
+ def is_block_device(self) -> bool:
989
+ """
990
+ 检查是否是块设备
991
+
992
+ 返回:
993
+ 如果是块设备返回True,否则返回False
994
+ """
995
+ return stat.S_ISBLK(self.stat().st_mode)
996
+
997
+ def is_char_device(self) -> bool:
998
+ """
999
+ 检查是否是字符设备
1000
+
1001
+ 返回:
1002
+ 如果是字符设备返回True,否则返回False
1003
+ """
1004
+ return stat.S_ISCHR(self.stat().st_mode)
1005
+
1006
+ def expanduser(self) -> 'PosixPath':
1007
+ """
1008
+ 展开用户目录(~)
1009
+
1010
+ 返回:
1011
+ 展开后的新Path对象
1012
+ """
1013
+ return self.__class__(os.path.expanduser(str(self)))
1014
+
1015
+ def is_hidden(self) -> bool:
1016
+ """
1017
+ 检查文件是否隐藏(以点开头)
1018
+
1019
+ 返回:
1020
+ 如果隐藏返回True,否则返回False
1021
+ """
1022
+ return self.name.startswith('.')
1023
+
1024
+
1025
+ class WindowsPath(Path, PureWindowsPath):
1026
+ """
1027
+ Windows系统专用路径实现
1028
+
1029
+ 仅在Windows系统上可用
1030
+ """
1031
+
1032
+ def __new__(cls, *args, **kwargs):
1033
+ if os.name != 'nt':
1034
+ raise NotImplementedError("WindowsPath 仅支持Windows系统")
1035
+ return super().__new__(cls, *args, **kwargs)
1036
+
1037
+ def resolve(self, strict=False) -> 'WindowsPath':
1038
+ """
1039
+ 解析绝对路径(Windows专用)
1040
+
1041
+ 参数:
1042
+ strict: 如果路径不存在是否抛出异常
1043
+
1044
+ 返回:
1045
+ 解析后的新Path对象
1046
+
1047
+ 异常:
1048
+ FileNotFoundError: 如果路径不存在且strict为True
1049
+ """
1050
+ path = os.path.abspath(str(self))
1051
+ if strict and not os.path.exists(path):
1052
+ raise FileNotFoundError(path)
1053
+ return self.__class__(path)
1054
+
1055
+ def owner(self) -> str:
1056
+ """
1057
+ 获取文件所有者(需要pywin32)
1058
+
1059
+ 返回:
1060
+ 所有者字符串(格式: 'DOMAIN\\username')
1061
+
1062
+ 异常:
1063
+ NotImplementedError: 如果没有安装pywin32
1064
+ OSError: 如果无法获取所有者信息
1065
+ """
1066
+ try:
1067
+ import win32security
1068
+ sd = win32security.GetFileSecurity(
1069
+ str(self), win32security.OWNER_SECURITY_INFORMATION
1070
+ )
1071
+ sid = sd.GetSecurityDescriptorOwner()
1072
+ name, domain, _ = win32security.LookupAccountSid(None, sid)
1073
+ return f"{domain}\\{name}"
1074
+ except ImportError:
1075
+ raise NotImplementedError("需要win32security模块")
1076
+
1077
+ def is_junction(self) -> bool:
1078
+ """
1079
+ 检查是否是junction点
1080
+
1081
+ 返回:
1082
+ 如果是junction点返回True,否则返回False
1083
+ """
1084
+ try:
1085
+ return os.path.isjunction(str(self))
1086
+ except AttributeError: # Python < 3.12 兼容
1087
+ try:
1088
+ import win32file
1089
+ return win32file.GetFileAttributes(str(self)) & win32file.FILE_ATTRIBUTE_REPARSE_POINT
1090
+ except ImportError:
1091
+ return False
1092
+
1093
+ def is_mount(self) -> bool:
1094
+ """
1095
+ 检查是否是挂载点(网络驱动器)
1096
+
1097
+ 返回:
1098
+ 如果是挂载点返回True,否则返回False
1099
+ """
1100
+ try:
1101
+ import win32file
1102
+ drive = os.path.splitdrive(str(self))[0]
1103
+ if not drive:
1104
+ return False
1105
+ return win32file.GetDriveType(drive) == win32file.DRIVE_REMOTE
1106
+ except ImportError:
1107
+ return False
1108
+
1109
+ def expanduser(self) -> 'WindowsPath':
1110
+ """
1111
+ Windows用户目录展开
1112
+
1113
+ 返回:
1114
+ 展开后的新Path对象
1115
+ """
1116
+ if self.drive or self.root:
1117
+ return self
1118
+ home = os.path.expanduser("~")
1119
+ return self.__class__(home) / self
1120
+
1121
+ def is_hidden(self) -> bool:
1122
+ """
1123
+ 检查文件是否隐藏(系统隐藏属性或以点开头)
1124
+
1125
+ 返回:
1126
+ 如果隐藏返回True,否则返回False
1127
+ """
1128
+ try:
1129
+ import win32api
1130
+ return win32api.GetFileAttributes(str(self)) & win32api.FILE_ATTRIBUTE_HIDDEN
1131
+ except ImportError:
1132
+ return self.name.startswith('.') or super().is_hidden()
1133
+
1134
+
1135
+ # 根据系统选择默认Path实现
1136
+ if os.name == 'nt':
1137
+ Path = WindowsPath
1138
+ else:
1139
+ Path = PosixPath