Skip to content

Commit c63d2b2

Browse files
committed
fix path2str and more powerfull SSHPath.glob
1 parent 29c14dc commit c63d2b2

File tree

4 files changed

+132
-22
lines changed

4 files changed

+132
-22
lines changed

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ ssh-utilities
3939
.. image:: https://img.shields.io/pypi/l/ssh-utilities
4040
:alt: PyPI - License
4141

42+
4243
.. |yes| unicode:: U+2705
4344
.. |no| unicode:: U+274C
4445
.. _builtins: https://docs.python.org/3/library/builtins.html
@@ -332,3 +333,4 @@ TODO
332333
----
333334
- implement wrapper for pool of connections
334335
- show which methods are implemented
336+
- SSHPath root and anchor attributes incorectlly return '.' instead of '/'

ssh_utilities/abc/_connection.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,16 @@ def _path2str(path: Optional["_SPATH"]) -> str:
112112
"""
113113
if isinstance(path, Path): # (Path, SSHPath)):
114114
p = fspath(path)
115-
return p if not p.endswith("/") else p[:-1]
116115
elif isinstance(path, str):
117-
return path if not path.endswith("/") else path[:-1]
116+
p = path
118117
else:
119118
raise FileNotFoundError(f"{path} is not a valid path")
120119

120+
if p.endswith("/") and len(p) > 1:
121+
return p[:-1]
122+
else:
123+
return p
124+
121125
@abstractmethod
122126
def to_dict(self):
123127
raise NotImplementedError

ssh_utilities/remote/path.py

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
"""Implements Path-like object for remote hosts."""
22

33
import logging
4-
import re
54
import stat
5+
from collections import deque
6+
from fnmatch import fnmatch
67
from functools import wraps
78
from os import fspath
8-
from os.path import join
99
from pathlib import Path, PurePosixPath, PureWindowsPath # type: ignore
10-
from typing import TYPE_CHECKING, Any, Callable, Generator, Optional, Union
10+
from typing import (TYPE_CHECKING, Any, Callable, Deque, Generator, Optional,
11+
Union)
1112

12-
from ..utils import for_all_methods, glob2re
13+
from ..utils import for_all_methods
1314

1415
if TYPE_CHECKING:
1516
from paramiko.sftp_attr import SFTPAttributes
@@ -113,7 +114,6 @@ def __new__(cls, connection: "SSHConnection", *args, **kwargs):
113114
else:
114115
cls._flavour = PurePosixPath._flavour # type: ignore
115116
except AttributeError as e:
116-
print(e)
117117
log.exception(e)
118118

119119
self = cls._from_parts(args, init=False) # type: ignore
@@ -234,31 +234,108 @@ def glob(self, pattern: str) -> Generator["SSHPath", None, None]:
234234
------
235235
FileNotFoundError
236236
if current path does not point to directory
237+
NotImplementedError
238+
when non-relative pattern is passed in
237239
238240
Warnings
239241
--------
240242
This method follows symlinks by default
241243
"""
242244
if not self.is_dir():
243245
raise FileNotFoundError(f"Directory {self} does not exist.")
244-
245-
if pattern.startswith("**"):
246+
if pattern.startswith("/"):
247+
raise NotImplementedError("Non-relative patterns are unsupported")
248+
249+
# first count shell pattern symbols if more than two are present,
250+
# search must be recursive
251+
pattern_count = sum([
252+
pattern.count("*"),
253+
pattern.count("?"),
254+
min((pattern.count("["), pattern.count("]")))
255+
])
256+
257+
if pattern_count >= 2:
246258
recursive = True
247259
else:
248260
recursive = False
249261

250-
pattern = glob2re(pattern)
262+
# split pattern to parts, so we can easily match at each subdir level
263+
parts: Deque[str] = deque(SSHPath(self.c, pattern).parts)
251264

265+
# append to origin path that parts of pattern that do not contain any
266+
# wildcards, so we search as minimal number of sub-directories as
267+
# possible
268+
while True:
269+
p = parts.popleft()
270+
if "*" in p or "?" in p or ("[" in p and "]" in p):
271+
parts.appendleft(p)
272+
break
273+
else:
274+
self /= p
275+
276+
# precompute number of origin path parts and pattern parts for speed
277+
origin_parts = len(self.parts)
278+
pattern_parts = len(parts) - 1
252279
for root, dirs, files in self.c.os.walk(self, followlinks=True):
253280

254-
for path in dirs + files:
255-
path = join(root, path)
256-
if re.search(pattern, f"/{path}", re.I):
257-
yield SSHPath(self.c, path)
281+
# compute number of actual root path parts
282+
root_parts = len(SSHPath(self.c, root).parts)
283+
# the difference determines which path of pattern to use, this is
284+
# because walk traverses directories "depth-first"
285+
idx = root_parts - origin_parts
286+
287+
# if we do not have the last part we are interested only in
288+
# directories because we need to get deeper in to the directory
289+
# structure
290+
if idx < pattern_parts:
291+
pattern = parts[idx]
292+
293+
# now get directories that match the pattern and delete the
294+
# others, tish takes advantage of list mutability - the next
295+
# seach paths will be built by walk based on already filtered
296+
# directories list
297+
indices = []
298+
for i, d in enumerate(dirs):
299+
if not fnmatch(d, pattern):
300+
indices.append(i)
301+
302+
for index in sorted(indices, reverse=True):
303+
del dirs[index]
304+
305+
elif idx >= pattern_parts:
306+
pattern = parts[-1]
307+
r = SSHPath(self.c, root)
308+
for path in dirs + files:
309+
if fnmatch(path, pattern):
310+
yield r / path
258311

259312
if not recursive:
260313
break
261314

315+
def group(self) -> str:
316+
"""Return file group.
317+
318+
Returns
319+
-------
320+
str
321+
string with group name
322+
323+
Raises
324+
------
325+
NotImplementedError
326+
when used on windows host
327+
"""
328+
if self.c.os.name == "nt":
329+
raise NotImplementedError("This is implemented only for posix "
330+
"type systems")
331+
else:
332+
cmd = ["stat", "-c", "'%G'", str(self)]
333+
group = self.c.subprocess.run(cmd, suppress_out=True, quiet=True,
334+
capture_output=True,
335+
encoding="utf-8").stdout
336+
337+
return group
338+
262339
def is_dir(self) -> bool:
263340
"""Check if path points to directory.
264341
@@ -404,6 +481,30 @@ def open(self, mode: str = "r", buffering: int = -1, # type: ignore
404481
self.c.builtins.open(self, mode=mode, bufsize=buffering,
405482
encoding=encoding)
406483

484+
def owner(self):
485+
"""Return file owner.
486+
487+
Returns
488+
-------
489+
str
490+
string with owner name
491+
492+
Raises
493+
------
494+
NotImplementedError
495+
when used on windows host
496+
"""
497+
if self.c.os.name == "nt":
498+
raise NotImplementedError("This is implemented only for posix "
499+
"type systems")
500+
else:
501+
cmd = ["getent", "passwd", self.stat().st_uid, "|", "cut", "-d:", "-f1"]
502+
owner = self.c.subprocess.run(cmd, suppress_out=True, quiet=True,
503+
capture_output=True,
504+
encoding="utf-8").stdout
505+
506+
return owner
507+
407508
def read_bytes(self) -> bytes:
408509
"""Read contents of a file as bytes.
409510
@@ -541,6 +642,10 @@ def symlink_to(self, target: "_SPATH", target_is_directory: bool = False):
541642
target path to which symlink will point
542643
target_is_directory: bool
543644
this parameter is ignored
645+
646+
Warnings
647+
--------
648+
`target_is_directory` parameter is ignored
544649
"""
545650
self.c.sftp.symlink(self._2str, fspath(target))
546651

@@ -632,14 +737,8 @@ def write_text(self, data, encoding: Optional[str] = None,
632737
f.write(data)
633738

634739
# ! NOT IMPLEMENTED
635-
def group(self):
636-
raise NotImplementedError
637-
638740
def link_to(self, target):
639741
raise NotImplementedError
640742

641-
def owner(self):
642-
raise NotImplementedError
643-
644743
def is_mount(self):
645744
raise NotImplementedError

tests/manual_test.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
from copy import deepcopy
44
import pickle
55

6+
c = Connection.get("kohn", quiet=True, local=False)
7+
8+
pat = "home/rynik/Raid/dizertacka/train_Si/ge_DPMD/train/gen?/train[0-9]/ge_all_*.pb"
9+
10+
print(list(c.pathlib.Path("/").glob(pat)))
11+
12+
"""
613
c = Connection.get("kohn", quiet=True, local=False)
714
print(c.os.isfile("/home/rynik/hw_config_Kohn.log"))
815
@@ -56,8 +63,6 @@
5663
c = Connection.from_str(con, quiet=True)
5764
print(c.os.isfile("/home/rynik/hw_config_Kohn.log"))
5865
59-
60-
"""
6166
A = TypeVar("A")
6267
B = TypeVar("B")
6368
T = TypeVar("T")

0 commit comments

Comments
 (0)