This commit is contained in:
kendfss 2021-08-12 19:28:02 +01:00
parent 9a67abbe20
commit 0d77b9a67d
4 changed files with 174 additions and 82 deletions

View File

@ -27,10 +27,10 @@ TODO
Caches for pickling (backup simplification)
Remove Size from repr for Directories. Large ones take too long to initialize
"""
__all__ = 'forbiddens Thing Place Thing Path Address Directory Folder File Library'.split()
__all__ = 'forbiddens Thing Place Thing Path Address Directory Folder File Library Scanner'.split()
from itertools import chain
from typing import Dict, Iterable, Iterator, List, Sequence, TypeAlias
from typing import Iterable, Iterator, TypeAlias, Callable
from warnings import warn
import io, os, pathlib, re, sys, shutil
@ -155,7 +155,8 @@ class Thing:
name = type(self).__name__.split('.')[-1]
size = f", size={self.size}" if self.isfile or isinstance(self, File) else ''
real = (f", real={self.exists}", '')[self.exists]
return f"{name}(name={self.name}, dir={self.up.name}{size}{real})"
up = f", dir={self.dir.name}" if self.dir.path != self.path else ''
return f"{name}(name={self.name}{up}{size}{real})"
def create(self, content:str|bytes|bytearray=None, raw:bool=False, exist_ok:bool=True, mode:int=511) -> _Path:
"""
@ -206,7 +207,7 @@ class Thing:
Determine if self.path points to a file or folder and create the corresponding object
"""
if self.isfile:
return Thing(self.path)
return File(self.path)
elif self.isdir:
return Place(self.path)
else:
@ -223,17 +224,18 @@ class Thing:
"""
Return the name of the referent
"""
return os.path.split(self.path)[1]
path, nome = os.path.split(self.path)
return nome if nome else path
@property
def ancestors(self) -> tuple:
def ancestors(self) -> tuple[_Place]:
"""
Return consecutive ADirs until the ADrive is reached
"""
level = []
p = self.path
while p != delevel(p):
p = delevel(p)
while p != shell.delevel(p):
p = shell.delevel(p)
level.append(p)
return tuple(Thing(i).obj for i in level)[::-1]
@property
@ -243,7 +245,7 @@ class Thing:
"""
return (i for i in self.up if isinstance(i, type(self)))
@property
def neighbours(self) -> tuple[_File, _Place]:
def neighbours(self) -> tuple[_Placefile]:
"""
Everything in the same Place
"""
@ -269,13 +271,15 @@ class Thing:
@property
def ancestry(self) -> str:
"""
A fancy representation of the tree from the apparent drive up to the given path
A nice representation of the tree from the apparent drive up to the given path
"""
print(f'ancestry({self.name})')
tree = f'ancestry({self.name})'
print(tree)
ancs = list(self.ancestors[1:])
ancs.append(self.path)
for i, anc in enumerate(ancs):
print('\t' + ('', '.' * i)[i > 0] + i * ' ' + [i for i in str(anc).split(os.sep) if i][-1] + '/')
line = '\n\t' + ('', '.' * i)[i > 0] + i * ' ' + [i for i in str(anc).split(os.sep) if i][-1] + '/'
print(line)
return self
@property
def isempty(self):
@ -308,7 +312,7 @@ class Thing:
path
return (True -> string, False -> Thing-like object)
"""
return delevel(self.path, steps) if path else Place(delevel(self.path, steps))
return shell.delevel(self.path, steps) if path else Place(shell.delevel(self.path, steps))
def touch(self) -> _Path:
"""
Implements the unix command 'touch', which updates the 'date modified' of the content at the path
@ -346,7 +350,7 @@ class Thing:
else:
new = self.path
new = shell.namespacer(new, sep=sep)
os.makedirs(delevel(new), exist_ok=True)
os.makedirs(shell.delevel(new), exist_ok=True)
copier(self.path, new)
out = Thing(new).obj
return out.touch() if touch else out
@ -459,16 +463,19 @@ class Place(Thing):
Place('.') == Place(os.getcwd())
"""
def __init__(self, path:str='NewPlace'):
if path=='.':
path = os.getcwd()
elif path == 'NewPlace':
path = shell.namespacer(path)
elif path == '~':
path = os.path.expanduser(path)
path = os.path.abspath(shell.trim(path))
if os.path.isfile(path):
raise ValueError("Given path corresponds to a file")
self.path = os.path.realpath(path)
if path.startswith(r"\\wsl$"):
self.path = path
else:
if path=='.':
path = os.getcwd()
elif path == 'NewPlace':
path = shell.namespacer(path)
elif path == '~':
path = os.path.expanduser(path)
path = os.path.abspath(shell.trim(path))
self.path = os.path.realpath(path)
super(type(self), self).__init__(path)
self.index = -1
def __len__(self):
@ -480,7 +487,7 @@ class Place(Thing):
return len(os.listdir(self.path)) > 0
def __iter__(self) -> Iterator[_Placefile]:
return self
def __next__(self) -> Sequence[_Placefile]:
def __next__(self) -> _Placefile:
if self.index<len(self)-1:
self.index += 1
return self.content[self.index]
@ -626,7 +633,7 @@ class Place(Thing):
return not self.depth
def add(self, other:Address, copy:bool=False) -> _Place:
def add(self, other:Thing, copy:bool=False) -> _Place:
"""
Introduce new elements. Send an address-like object to self.
"""
@ -684,6 +691,87 @@ class Audio(File):
@property
def title(self):
return self.tags['title']
class Scanner:
"""
Object which scans text files, and their names, for given keywords
example
>>> s = Scanner("f = ma")
>>> s("./principia.txt", strict=True)
True
>>> s("./bible.txt", strict=True)
False
>>> s("./principia.txt", strict=False)
True
>>> s("./bible.txt", strict=False)
True
"""
def __init__(self, keywords:str, mode:str='r', strict:bool=True, prescaped:bool=False, case:bool=False, opener:Callable=open, lines:bool=True):
"""
params:
keywords
terms to search for
mode
'r' or 'rb'
strict
True -> search for words
False -> clauses
prescaped
whether or not terms have already been regex escaped
case
true -> case sensitive
opener
must return an object with a "readlines" or "read" method (depends on lines)
lines
wheter or not to scan by lines
"""
self.__case = case
self.__keywords = keywords
self.opener = opener
self.lines = lines
self.mode = mode
self.strict = strict
self.prescaped = prescaped
@property
def keywords(self):
"""
handles any necessary escaping
"""
return re.escape(self.__keywords) if not self.prescaped else self.__keywords
@property
def case(self):
"""
standardize the case-fold setting
"""
return re.I if not self.__case else 0
@property
def pattern(self):
"""
compile the search pattern
"""
return re.compile(
(
'|'.join(self.keywords.split()),
self.keywords
)[self.strict],
self.case
)
def __call__(self, path:str, lines:bool=None) -> bool:
"""
Scan a file at a given path for a predefined word/clause, you can also override the default lines argument
"""
if isinstance(lines, type(None)):
lines = self.lines
try:
with self.opener(path, self.mode) as fob:
method = (fob.read, fob.readlines)[lines]
return self.pattern.search(path) or any(map(self.pattern.search, method()))
except UnicodeDecodeError:
return False
if __name__ == '__main__':
# show(locals().keys())

View File

@ -1,4 +1,4 @@
# __all__ = "discard unarchive create_enter audio_or_video namespacer isempty move send2trash ffplay trim move_file delevel convert cat".split()
# __all__ = "discard unarchive create_enter audio_or_video NameSpacer namespacer isempty move send2trash ffplay trim move_file delevel convert cat".split()
__all__ = "mcd mv move_file rm ffplay convert cat".split()
from itertools import filterfalse
@ -30,44 +30,39 @@ def delevel(path:str, steps:int=1) -> str:
steps -= 1
return path if not path.endswith(':') else path+os.sep
def namespacer(path:str, sep:str='_', start:int=2) -> str:
class NameSpacer:
"""
Returns a unique version of a given string by appending an integer
Create an incrementation of a path if it already exists on the local system
example:
tree:
/folder
/file.ext
/file_2.ext
>>> namespacer('file', sep='-', start=2)
file-2.ext
>>> namespacer('file', sep='_', start=2)
file_3.ext
>>> namespacer('file', sep='_', start=0)
file_0.ext
params
format
a formattable string amenable to both "name" and "index" key words
examples
>>> my_path = "c:/users"
>>> NameSpacer()(my_path)
c:/users_2
>>> NameSpacer("{name} ({index})")(my_path)
c:/users (2)
"""
id = start
oldPath = path[:]
while os.path.exists(path):
newPath = list(os.path.splitext(path))
if sep in newPath[0]:
if newPath[0].split(sep)[-1].isnumeric():
# print('case1a')
id = newPath[0].split(sep)[-1]
newPath[0] = newPath[0].replace(f'{sep}{id}', f'{sep}{str(int(id)+1)}')
path = ''.join(newPath)
else:
# print('case1b')
newPath[0] += f'{sep}{id}'
path = ''.join(newPath)
id += 1
else:
# print('case2')
newPath[0] += f'{sep}{id}'
path = ''.join(newPath)
id += 1
return path
def __init__(self, format:str="_{index}"):
self.format = format
def __call__(self, path:str, index:int=2) -> str:
if not os.path.exists(path):
return path
if os.path.exists(new:=self.new(path, index)):
return self(path, index + 1)
return new
def new(self, path:str, index:int) -> str:
weirdos = ".tar".split()
name, ext = os.path.splitext(path)
while any(name.endswith(weirdo:=w) for w in weirdos):
if name != (name:=name.removesuffix(weirdo)):
ext = weirdo + ext
return name + self.format.format(index=index) + ext
namespacer = NameSpacer()
def trim(path:str, edge:str=os.sep) -> str:
"""
@ -123,7 +118,7 @@ def create_enter(*args:[str, Iterable[str]], go_back:bool=False, recursive:bool=
"""
home = os.getcwd()
for arg in flat(args):
arg = nameSpacer(arg) if arg!='..' and not overwrite else arg
arg = namespacer(arg) if arg!='..' and not overwrite else arg
os.makedirs(arg, exist_ok=exist_ok)
os.chdir(arg if recursive else home)
last_stop = home if go_back else os.getcwd()
@ -150,9 +145,9 @@ def move(source:str, dest:str, make_dest:bool=False) -> str:
if not make_dest:
raise ValueError(f"Destination's path doesn't point to a directory")
os.makedirs(dest, exist_ok=True)
root, name = os.path.split(path)
root, name = os.path.split(source)
new = os.path.join(dest, name)
os.rename(path, new)
os.rename(source, new)
return new
mv = move
@ -337,7 +332,7 @@ def isempty(path:str, make:bool=False) -> bool:
with open(path, 'rb') as f:
return not bool(len(tuple(i for i in f)))
elif os.path.isdir(path):
return not bool(len(os.listdir(file)))
return not bool(len(os.listdir(path)))
elif make:
if os.path.splitext(path)[-1]:
x = open(path, 'x')

View File

@ -7,6 +7,9 @@ from itertools import permutations, chain
import os, re
from sl4ng import pop, show, multisplit, join, mainame, eq
LOCKOUTS = "Config.Msi*System Volume Information*$Recycle.Bin*C:\\Users\\Administrator".lower().split('*')
def walk(root:str='.', dirs:bool=False, absolute:bool=True) -> Iterator[str]:
"""
Walk a directory's tree yielding paths to any files and/or folders along the way
@ -29,7 +32,7 @@ def walk(root:str='.', dirs:bool=False, absolute:bool=True) -> Iterator[str]:
root = (str, os.path.realpath)[absolute](str(root))
for name in os.listdir(root):
path = os.path.join(root, name)
if os.path.isdir(path):
if os.path.isdir(path) and not name.lower() in LOCKOUTS:
if dirs:
yield (name, path)[absolute]
yield from walk(path, dirs=dirs, absolute=absolute)
@ -67,12 +70,14 @@ def files(root:str='.', exts:str='', negative:bool=False, absolute:bool=True) ->
root = (str, os.path.realpath)[absolute](str(root))
pat = parse_extensions(exts)
predicate = lambda x: not bool(pat.search(x)) if negative else bool(pat.search(x))
for name in os.listdir(root):
path = os.path.join(root, name)
if os.path.isdir(path):
yield from files(path, exts=exts, negative=negative, absolute=absolute)
elif predicate(path):
yield (name, path)[absolute]
name = os.path.split(root)[1]
if not (name.lower() in LOCKOUTS or root.lower() in LOCKOUTS):
for name in os.listdir(root):
path = os.path.join(root, name)
if os.path.isdir(path) and not name.lower() in LOCKOUTS:
yield from files(path, exts=exts, negative=negative, absolute=absolute)
elif predicate(path):
yield (name, path)[absolute]
def folders(root:str='.', absolute:bool=True) -> Iterator[str]:
"""
@ -94,7 +99,7 @@ def folders(root:str='.', absolute:bool=True) -> Iterator[str]:
root = (str, os.path.realpath)[absolute](str(root))
for name in os.listdir(root):
path = os.path.join(root, name)
if os.path.isdir(path):
if os.path.isdir(path) and not name.lower() in LOCKOUTS:
yield (name, path)[absolute]
yield from folders(path, absolute=absolute)
@ -248,4 +253,4 @@ if __name__ == "__main__":
# box4 = [*folders(folder, True)]
# show(box4, 0, 1)
# show(search(folder, '__init__'))
show(search(folder, '_', 'png'))
show(search(folder, '_', 'png'))

View File

@ -3,12 +3,15 @@ from setuptools import setup, find_packages
with open('README.md', 'r') as fob:
long_description = fob.read()
with open('requirements.txt', 'r') as fob:
requirements = fob.readlines()
setup(
name='filey',
version='0.0.2',
author='Kenneth Sabalo',
author_email='kennethsantanasablo@gmail.com',
url='https://tildegit.org/eli2and40/filey',
url='https://github.com/kendfss/filey',
packages=['filey'],
classifiers=[
"Programming Language :: Python :: 3",
@ -19,13 +22,14 @@ setup(
long_description_content_type='text/markdown',
keywords='utilities operating path file system',
license='MIT',
requires=[
"audio_metadata",
"dill",
"filetype",
"send2trash",
"sl4ng",
"pypeclip",
],
requires=requirements,
# requires=[
# "audio_metadata",
# "dill",
# "filetype",
# "send2trash",
# "sl4ng",
# "pypeclip",
# ],
python_requires='>3.9',
)