├── docs ├── exampledir │ ├── Vogel.txt │ ├── jowly.pdf │ ├── monkish.txt │ ├── scrooge │ │ ├── light.pdf │ │ ├── reliquary.pdf │ │ └── paycheck │ │ │ └── electrophoresis.txt │ └── pedantic │ │ └── cataclysmic.txt ├── templates │ ├── credits.mako │ ├── head.mako │ ├── logo.mako │ ├── _lunr_search.inc.mako │ ├── config.mako │ ├── text.mako │ ├── search.mako │ ├── pdf.mako │ ├── css.mako │ └── html.mako ├── seedir │ ├── errors.html │ └── printing.html └── gettingstarted.md ├── MANIFEST.in ├── seedir ├── __version__.py ├── errors.py ├── __init__.py ├── __main__.py ├── printing.py ├── realdir.py └── folderstructure.py ├── .gitattributes ├── img ├── pun.jpg ├── seedir_diagram.png └── seedir_diagram.pptx ├── pdoc_command.txt ├── tests ├── __init__.py ├── print_examples.py └── test_seedir.py ├── stackoverflow.txt ├── .gitignore ├── pyproject.toml ├── .github └── workflows │ ├── deploy_pypi.yml │ ├── deploy_testpypi.yml │ └── test.yml ├── LICENSE ├── setup.py ├── README.md ├── CONTRIBUTING.md └── CHANGELOG.md /docs/exampledir/Vogel.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/exampledir/jowly.pdf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/exampledir/monkish.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/templates/credits.mako: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/templates/head.mako: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/templates/logo.mako: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/exampledir/scrooge/light.pdf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include seedir/words.txt -------------------------------------------------------------------------------- /docs/exampledir/scrooge/reliquary.pdf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /seedir/__version__.py: -------------------------------------------------------------------------------- 1 | version='0.5.1' -------------------------------------------------------------------------------- /docs/exampledir/pedantic/cataclysmic.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/exampledir/scrooge/paycheck/electrophoresis.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.py linguist-vendored=false 3 | -------------------------------------------------------------------------------- /img/pun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earnestt1234/seedir/HEAD/img/pun.jpg -------------------------------------------------------------------------------- /pdoc_command.txt: -------------------------------------------------------------------------------- 1 | pdoc --html -o docs --template-dir docs/templates seedir --force -------------------------------------------------------------------------------- /img/seedir_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earnestt1234/seedir/HEAD/img/seedir_diagram.png -------------------------------------------------------------------------------- /img/seedir_diagram.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earnestt1234/seedir/HEAD/img/seedir_diagram.pptx -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Sep 7 11:53:42 2020 4 | 5 | @author: earne 6 | """ 7 | -------------------------------------------------------------------------------- /seedir/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Custom exceptions for seedir. 4 | 5 | """ 6 | 7 | # NOTE: SeedirError removed for 0.4.0 8 | 9 | # class SeedirError(Exception): 10 | # """Class for representing errors from module `seedir.realdir`""" 11 | 12 | class FakedirError(Exception): 13 | """Class for handling errors from module `seedir.fakedir`""" 14 | -------------------------------------------------------------------------------- /stackoverflow.txt: -------------------------------------------------------------------------------- 1 | Stack Overflow posts which either inspired or helped solve issues with seedir: 2 | 3 | posts about printing tree structures in Python: https://stackoverflow.com/q/9727673/13386979 4 | posts about printing tree structures in general: https://stackoverflow.com/q/19699059/13386979 5 | recursively adding to a string: https://stackoverflow.com/a/44335113/13386979 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #testing script 2 | seedir/seedir_testing.py 3 | seedirpackagetesting.py 4 | 5 | # Compiled python modules. 6 | *.pyc 7 | seedir/__pycache__ 8 | 9 | # Setuptools distribution folder. 10 | /dist/ 11 | /build/ 12 | 13 | # Python egg metadata, regenerated from source files by setuptools. 14 | /*.egg-info 15 | 16 | # Mac specific editing files 17 | .DS_Store 18 | **/.DS_Store 19 | 20 | .ipynb_checkpoints 21 | */.ipynb_checkpoints/* 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "seedir" 7 | description = "Package for creating, editing, and reading folder tree diagrams." 8 | dependencies = [ 9 | "natsort" 10 | ] 11 | dynamic = ["version"] 12 | authors =[ 13 | {name = "Tom Earnest", email = "earnestt1234@gmail.com"} 14 | ] 15 | readme = "README.md" 16 | license = {text = "MIT"} 17 | 18 | [project.optional-dependencies] 19 | emoji = ["emoji"] 20 | 21 | [project.urls] 22 | Homepage = "https://github.com/earnestt1234/seedir" 23 | 24 | [project.scripts] 25 | seedir = "seedir.__main__:main" -------------------------------------------------------------------------------- /.github/workflows/deploy_pypi.yml: -------------------------------------------------------------------------------- 1 | # DERIVED FROM: https://realpython.com/github-actions-python 2 | 3 | # human readable name (shows up on Github) 4 | name: Deploy code to PyPI. 5 | 6 | on: 7 | release: 8 | types: [published] 9 | workflow_call: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.13" 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | python -m pip install build 26 | 27 | - name: Build package 28 | run: python -m build 29 | 30 | - name: Upload to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/deploy_testpypi.yml: -------------------------------------------------------------------------------- 1 | # DERIVED FROM: https://realpython.com/github-actions-python 2 | 3 | # human readable name (shows up on Github) 4 | name: Deploy code to TestPyPI. 5 | 6 | on: 7 | release: 8 | types: [published] 9 | workflow_call: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.13" 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | python -m pip install build 26 | 27 | - name: Build package 28 | run: python -m build 29 | 30 | - name: Upload to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.TESTPYPI_API_TOKEN }} 35 | repository-url: https://test.pypi.org/legacy/ -------------------------------------------------------------------------------- /seedir/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | [seedir](https://github.com/earnestt1234/seedir) is a Python package for 4 | creating, editing, and reading folder tree diagrams. 5 | 6 | The general logic of seedir is based on representing directories in 3 7 | different forms: real directories on a computer, text diagrams of directories, 8 | and "fake directories" (manipulable Python folder objects). seedir provides 9 | tools for going in between these formats. 10 | 11 | .. include:: ../docs/gettingstarted.md 12 | 13 | ## Cheatsheet 14 | 15 | ![](https://raw.githubusercontent.com/earnestt1234/seedir/master/img/seedir_diagram.png) 16 | 17 | """ 18 | 19 | #imports for package namespace 20 | from .realdir import seedir 21 | 22 | from .fakedir import (fakedir, 23 | fakedir_fromstring, 24 | populate, 25 | randomdir,) 26 | 27 | from .fakedir import FakeDir 28 | from .fakedir import FakeFile 29 | 30 | from .printing import (get_styleargs, 31 | STYLE_DICT,) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tom Earnest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Setup for seedir. 5 | 6 | @author: Tom Earnest 7 | """ 8 | 9 | from os import path 10 | 11 | from setuptools import setup 12 | 13 | # read the contents of your README file 14 | this_directory = path.abspath(path.dirname(__file__)) 15 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 16 | long_description = f.read() 17 | 18 | # read version 19 | with open(path.join(this_directory, 'seedir', '__version__.py'), encoding='utf-8') as f: 20 | version = f.read().split('=')[1].strip('\'"\n') 21 | 22 | setup(name='seedir', 23 | version=version, 24 | description='Package for creating, editing, and reading folder tree diagrams.', 25 | url='https://github.com/earnestt1234/seedir', 26 | author='Tom Earnest', 27 | author_email='earnestt1234@gmail.com', 28 | license='MIT', 29 | packages=['seedir'], 30 | install_requires=['natsort'], 31 | extras_require={'emoji': ['emoji']}, 32 | include_package_data=True, 33 | zip_safe=False, 34 | long_description=long_description, 35 | long_description_content_type='text/markdown', 36 | entry_points = { 37 | 'console_scripts': ['seedir=seedir.__main__:main'], 38 | }) 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | # defines when testing job is run 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - dev 9 | pull_request: 10 | branches: 11 | - main 12 | - dev 13 | workflow_call: 14 | workflow_dispatch: 15 | 16 | # Defines the tasks to be completed 17 | jobs: 18 | 19 | testing: 20 | runs-on: ubuntu-latest 21 | continue-on-error: true 22 | strategy: 23 | matrix: 24 | python-version: ["3.10", "3.11", "3.12", "3.13"] 25 | 26 | steps: 27 | - uses: actions/checkout@v4 # checkout the github branch 28 | 29 | # Sets up Python, accesising the matrix defined above 30 | # to get a specific Python version 31 | # adding name here allows Python version to be documented 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | cache: "pip" 37 | 38 | # Installs the code to be tested, and pytest 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip 42 | python -m pip install . 43 | python -m pip install pytest 44 | 45 | # Runs pytest 46 | - name: Run Pytest 47 | run: pytest -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seedir 2 | A Python package for creating, editing, and reading folder tree diagrams. 3 | 4 | ![](https://raw.githubusercontent.com/earnestt1234/seedir/master/img/pun.jpg) 5 | 6 | *Photo by [Adam Kring](https://unsplash.com/@adamkring).* 7 | 8 | 9 | ```python 10 | >>> import seedir as sd 11 | >>> sd.seedir(style='lines', itemlimit=10, depthlimit=2, exclude_folders='.git') 12 | seedir/ 13 | ├─.gitattributes 14 | ├─.gitignore 15 | ├─.ipynb_checkpoints/ 16 | │ └─examples-checkpoint.ipynb 17 | ├─build/ 18 | │ ├─bdist.win-amd64/ 19 | │ └─lib/ 20 | ├─CHANGELOG.md 21 | ├─dist/ 22 | │ └─seedir-0.1.4-py3-none-any.whl 23 | ├─docs/ 24 | │ ├─exampledir/ 25 | │ ├─gettingstarted.md 26 | │ ├─seedir/ 27 | │ └─templates/ 28 | ├─img/ 29 | │ ├─pun.jpg 30 | │ ├─seedir_diagram.png 31 | │ └─seedir_diagram.pptx 32 | ├─LICENSE 33 | └─MANIFEST.in 34 | 35 | ``` 36 | 37 | ## Installation 38 | 39 | Available with [`pip`](https://pypi.org/project/seedir/): 40 | 41 | ``` 42 | pip install seedir 43 | ``` 44 | 45 | To make use of the 'emoji' style, install [emoji](https://pypi.org/project/emoji/) (`pip install emoji`) or use `pip install seedir[emoji]`. 46 | 47 | ## Usage 48 | 49 | See the [API documentation](https://earnestt1234.github.io/seedir/seedir/index.html) (generated with [pdoc3](https://pdoc3.github.io/pdoc/)). 50 | 51 | ## License 52 | 53 | Open source under MIT. 54 | 55 | -------------------------------------------------------------------------------- /tests/print_examples.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Unit tests for seedir. 4 | 5 | Previously written for unittests, and updated for pytest. 6 | 7 | Test methods MUST start with "test" 8 | """ 9 | 10 | import os 11 | 12 | import seedir as sd 13 | 14 | # realdir for testing on 15 | testdir = os.path.dirname(os.path.abspath(__file__)) 16 | 17 | # ---- Test cases 18 | 19 | print('\n--------------------' 20 | '\n\nTesting seedir.seedir() against {}\n\n' 21 | '--------------------' 22 | '\n'.format(testdir)) 23 | 24 | print('Basic seedir (depthlimit=2, itemlimit=10):\n') 25 | sd.seedir(testdir, depthlimit=2, itemlimit=10) 26 | 27 | print('\nDifferent Styles (depthlimit=1, itemlimit=5):') 28 | for style in sd.STYLE_DICT.keys(): 29 | print('\n{}:\n'.format(style)) 30 | sd.seedir(testdir, style=style, depthlimit=1, itemlimit=5) 31 | 32 | print('\nCustom Styles (depthlimit=1, itemlimit=5):') 33 | sd.seedir(testdir, depthlimit=1, itemlimit=5, space='>>', 34 | split='>>', extend='II', final='->', 35 | folderstart='Folder: ', filestart='File: ') 36 | 37 | print('\nDifferent Indents (depthlimit=1, itemlimit=5):') 38 | for i in list(range(3)) + [8]: 39 | print('\nindent={}:\n'.format(str(i))) 40 | sd.seedir(testdir, depthlimit=1, itemlimit=5, indent=i) 41 | 42 | print('\nItems Beyond Limit (depthlimit=1, itemlimit=1, beyond="content")') 43 | sd.seedir(testdir, itemlimit=1, beyond='content') 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/templates/_lunr_search.inc.mako: -------------------------------------------------------------------------------- 1 |
2 | 4 |
5 | 6 | 7 | 21 | 51 | -------------------------------------------------------------------------------- /docs/templates/config.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | # Template configuration. Copy over in your template directory 3 | # (used with `--template-dir`) and adapt as necessary. 4 | # Note, defaults are loaded from this distribution file, so your 5 | # config.mako only needs to contain values you want overridden. 6 | # You can also run pdoc with `--config KEY=VALUE` to override 7 | # individual values. 8 | 9 | html_lang = 'en' 10 | show_inherited_members = False 11 | extract_module_toc_into_sidebar = True 12 | list_class_variables_in_index = True 13 | sort_identifiers = True 14 | show_type_annotations = True 15 | 16 | # Show collapsed source code block next to each item. 17 | # Disabling this can improve rendering speed of large modules. 18 | show_source_code = True 19 | 20 | # If set, format links to objects in online source code repository 21 | # according to this template. Supported keywords for interpolation 22 | # are: commit, path, start_line, end_line. 23 | #git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' 24 | #git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' 25 | #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' 26 | #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start-line}' 27 | git_link_template = None 28 | 29 | # A prefix to use for every HTML hyperlink in the generated documentation. 30 | # No prefix results in all links being relative. 31 | link_prefix = '' 32 | 33 | # Enable syntax highlighting for code/source blocks by including Highlight.js 34 | syntax_highlighting = True 35 | 36 | # Set the style keyword such as 'atom-one-light' or 'github-gist' 37 | # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles 38 | # Demo: https://highlightjs.org/static/demo/ 39 | hljs_style = 'github' 40 | 41 | # If set, insert Google Analytics tracking code. Value is GA 42 | # tracking id (UA-XXXXXX-Y). 43 | google_analytics = '' 44 | 45 | # If set, insert Google Custom Search search bar widget above the sidebar index. 46 | # The whitespace-separated tokens represent arbitrary extra queries (at least one 47 | # must match) passed to regular Google search. Example: 48 | #google_search_query = 'inurl:github.com/USER/PROJECT site:PROJECT.github.io site:PROJECT.website' 49 | google_search_query = '' 50 | 51 | # Enable offline search using Lunr.js. For explanation of 'fuzziness' parameter, which is 52 | # added to every query word, see: https://lunrjs.com/guides/searching.html#fuzzy-matches 53 | # If 'index_docstrings' is False, a shorter index is built, indexing only 54 | # the full object reference names. 55 | #lunr_search = {'fuzziness': 1, 'index_docstrings': True} 56 | lunr_search = None 57 | 58 | # If set, render LaTeX math syntax within \(...\) (inline equations), 59 | # or within \[...\] or $$...$$ or `.. math::` (block equations) 60 | # as nicely-formatted math formulas using MathJax. 61 | # Note: in Python docstrings, either all backslashes need to be escaped (\\) 62 | # or you need to use raw r-strings. 63 | latex_math = False 64 | %> 65 | -------------------------------------------------------------------------------- /docs/templates/text.mako: -------------------------------------------------------------------------------- 1 | ## Define mini-templates for each portion of the doco. 2 | 3 | <%! 4 | def indent(s, spaces=4): 5 | new = s.replace('\n', '\n' + ' ' * spaces) 6 | return ' ' * spaces + new.strip() 7 | %> 8 | 9 | <%def name="deflist(s)">:${indent(s)[1:]} 10 | 11 | <%def name="h3(s)">### ${s} 12 | 13 | 14 | <%def name="function(func)" buffered="True"> 15 | <% 16 | returns = show_type_annotations and func.return_annotation() or '' 17 | if returns: 18 | returns = ' \N{non-breaking hyphen}> ' + returns 19 | %> 20 | `${func.name}(${", ".join(func.params(annotate=show_type_annotations))})${returns}` 21 | ${func.docstring | deflist} 22 | 23 | 24 | <%def name="variable(var)" buffered="True"> 25 | <% 26 | annot = show_type_annotations and var.type_annotation() or '' 27 | if annot: 28 | annot = ': ' + annot 29 | %> 30 | `${var.name}${annot}` 31 | ${var.docstring | deflist} 32 | 33 | 34 | <%def name="class_(cls)" buffered="True"> 35 | `${cls.name}(${", ".join(cls.params(annotate=show_type_annotations))})` 36 | ${cls.docstring | deflist} 37 | <% 38 | class_vars = cls.class_variables(show_inherited_members, sort=sort_identifiers) 39 | static_methods = cls.functions(show_inherited_members, sort=sort_identifiers) 40 | inst_vars = cls.instance_variables(show_inherited_members, sort=sort_identifiers) 41 | methods = cls.methods(show_inherited_members, sort=sort_identifiers) 42 | mro = cls.mro() 43 | subclasses = cls.subclasses() 44 | %> 45 | % if mro: 46 | ${h3('Ancestors (in MRO)')} 47 | % for c in mro: 48 | * ${c.refname} 49 | % endfor 50 | 51 | % endif 52 | % if subclasses: 53 | ${h3('Descendants')} 54 | % for c in subclasses: 55 | * ${c.refname} 56 | % endfor 57 | 58 | % endif 59 | % if class_vars: 60 | ${h3('Class variables')} 61 | % for v in class_vars: 62 | ${variable(v) | indent} 63 | 64 | % endfor 65 | % endif 66 | % if static_methods: 67 | ${h3('Static methods')} 68 | % for f in static_methods: 69 | ${function(f) | indent} 70 | 71 | % endfor 72 | % endif 73 | % if inst_vars: 74 | ${h3('Instance variables')} 75 | % for v in inst_vars: 76 | ${variable(v) | indent} 77 | 78 | % endfor 79 | % endif 80 | % if methods: 81 | ${h3('Methods')} 82 | % for m in methods: 83 | ${function(m) | indent} 84 | 85 | % endfor 86 | % endif 87 | 88 | 89 | ## Start the output logic for an entire module. 90 | 91 | <% 92 | variables = module.variables(sort=sort_identifiers) 93 | classes = module.classes(sort=sort_identifiers) 94 | functions = module.functions(sort=sort_identifiers) 95 | submodules = module.submodules() 96 | heading = 'Namespace' if module.is_namespace else 'Module' 97 | %> 98 | 99 | ${heading} ${module.name} 100 | =${'=' * (len(module.name) + len(heading))} 101 | ${module.docstring} 102 | 103 | 104 | % if submodules: 105 | Sub-modules 106 | ----------- 107 | % for m in submodules: 108 | * ${m.name} 109 | % endfor 110 | % endif 111 | 112 | % if variables: 113 | Variables 114 | --------- 115 | % for v in variables: 116 | ${variable(v)} 117 | 118 | % endfor 119 | % endif 120 | 121 | % if functions: 122 | Functions 123 | --------- 124 | % for f in functions: 125 | ${function(f)} 126 | 127 | % endfor 128 | % endif 129 | 130 | % if classes: 131 | Classes 132 | ------- 133 | % for c in classes: 134 | ${class_(c)} 135 | 136 | % endfor 137 | % endif 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to seedir 3 | 4 | This document describes ways that you can contribute to seedir, progressing in increasing levels of involvement. 5 | 6 | ## Use it 7 | 8 | Simply using this package is helpful! It is rewarding for me to see more people using it, and makes me want to keep working on it. Having the code be used in more real world settings is great for detecting use cases and missing features. Maybe you are already using it if you are reading this... but if not, please give it a try. 9 | 10 | ## Share it 11 | 12 | If you liked seedir or found it useful, sharing it with others is a good way to help the package grow. Moreover, the package maintainer is basically just me - so sharing the project may be helpful for growing a maintenance community and fostering stable development in the long term. 13 | 14 | ## Report a bug / request a feature 15 | 16 | Please use the [issues page](https://github.com/earnestt1234/seedir/issues) to formally log any sort of suggestion to improve the package. This can be a bug report, a feature request, documentation improvements, etc. These are extremely helpful as they can give me a sense of what people are actually doing with the package, and what its drawbacks are. 17 | 18 | Note that proposed changes will be implemented at the discretion of the maintainers. 19 | 20 | There currently is no formal structure needed for posting an issue. But some general guidelines: 21 | 22 | - If sharing a bug, include a minimally reproducible example - something that I will be able to demonstrate and debug on my own machine. 23 | - Please share code where possible (as plaintext, not as pictures) 24 | - Please be courteous : ) 25 | 26 | ## Contribute code 27 | 28 | We welcome anyone who wants to contribute code to seedir! Expanding the maintainers would be great for the longterm stability of the package. 29 | 30 | This has not happened much so far, so the guidelines may evolve over time. But the general steps should be: 31 | 32 | 1. Open an issue (or comment on an existing issue) indicating the changes you plan to implement. Discuss with a maintainer to agree on a general course of action for the proposed changes. 33 | 2. [Fork the seedir repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo), and make sure you have the most up-to-date changes from the `dev` branch. 34 | 3. Create a branch stemming from the `dev` branch. Name it whatever you like, but ideally something related to the feature/bug you plan to tackle. 35 | 4. Implement your changes, making one or more commits to your created branch. **Note:** We currently do not have an opposition to generative AI tools (e.g. ChatGPT, Claude, Copilot, etc) being used for aiding with seedir development. But we ask that you inform us if you have used AI tools for any code slated to be contributed to the seedir codebase. 36 | 5. Run the test suite to make sure they still pass. Add any additional tests needed to cover your changes - or edit existing tests if they are no longer valid following your changes. 37 | 6. Add to the [`CHANGELOG.md`](https://github.com/earnestt1234/seedir/blob/master/CHANGELOG.md) to document any changes that occurred. You can add a header for "DEV" rather than assigning them to a specific version. 38 | 7. Create a pull request into the main branch, with a brief summary of what you did. 39 | 8. A maintainer should then approve your changes and merge them into `dev`, which will then be merged into `master` at the next version release. 40 | 41 | Please raise an issue if these steps are unclear or seem incorrect. If you are more familiar with this process and have suggestions, please let us know! 42 | 43 | -------------------------------------------------------------------------------- /seedir/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import os 6 | 7 | from .realdir import seedir 8 | 9 | helptxt = """ 10 | Help for the seedir CLI. Function for printing a folder tree 11 | structure, similar to UNIX `tree`. Directs all arguments to `seedir.seedir()`. 12 | 13 | Note that this has been revamped for version v0.3.1. Specifically, parsing 14 | was moved from getopts to argparse. Options should now all explicitly 15 | reference parameters of `seedir.seedir()`. 16 | 17 | Not all seedir arguments are accepted, namely ones which expect Python 18 | callables and ones which alter the style. The latter may be added 19 | in a future version. 20 | 21 | Options (short/long): 22 | 23 | -b / --beyond BEYOND 24 | Way to represent items past the depthlimit or itemlimit. 25 | Options are 'content', 'ellipsis', or explicit string starting 26 | with an underscore. Not set by default. 27 | 28 | -d / --depthlimit DEPTHLIMIT 29 | Integer limit on depth of folders to enter. Default: None 30 | 31 | -f / --first FIRST 32 | Sort the directory so that either files or folders appear first. 33 | Options are "folders" or "files". 34 | 35 | -h / --help 36 | Show this message and exit. 37 | 38 | -i / --itemlimit ITEMLIMIT 39 | Integer limit on the number of items to include per directory. 40 | Default: None 41 | 42 | -p / --path PATH 43 | Dir to see. Default: current directory (os.getcwd()) 44 | 45 | -r / --sort_reverse 46 | Reverse the sort. 47 | 48 | -s / --sort 49 | Sort the contents of each directory by name. 50 | 51 | -t / --indent INDENT 52 | Amount to indent. Default: 2 53 | 54 | -y / --style STYLE 55 | seedir style to use. Default: "lines" 56 | 57 | Options (long only): 58 | 59 | --include_folders, --include_files, --exclude_folders, --exclude_files ... 60 | Folder / file names to include or exclude. Not set by default. Pass 61 | multiple items as space-separated values. 62 | 63 | --regex 64 | Interpret include/exclude folder/file arguments as Python regex. 65 | """ 66 | 67 | def parse(): 68 | 69 | """Parse command line arguments with argparse.""" 70 | 71 | parser = argparse.ArgumentParser(add_help=False) 72 | 73 | parser.add_argument('-b', '--beyond', default=None) 74 | parser.add_argument('-d', '--depthlimit', default=None, type=int) 75 | parser.add_argument('-f', '--first', default=None) 76 | parser.add_argument('-h', '--help', action='store_true') 77 | parser.add_argument('-i', '--itemlimit', default=None, type=int) 78 | parser.add_argument('-p', '--path', default=os.getcwd()) 79 | parser.add_argument('-r', '--sort_reverse', action='store_true') 80 | parser.add_argument('-s', '--sort', action='store_true') 81 | parser.add_argument('-t', '--indent', default=2, type=int) 82 | parser.add_argument('-y', '--style', default='lines') 83 | 84 | parser.add_argument('--include_folders', default=None, nargs='+') 85 | parser.add_argument('--exclude_folders', default=None, nargs='+') 86 | parser.add_argument('--include_files', default=None, nargs='+') 87 | parser.add_argument('--exclude_files', default=None, nargs='+') 88 | parser.add_argument('--regex', action='store_true') 89 | 90 | return parser.parse_args() 91 | 92 | def main(): 93 | 94 | """Parses arguments and passes them to `seedir.seedir()`""" 95 | 96 | args = parse() 97 | if args.help: 98 | print(helptxt) 99 | return 100 | 101 | kwargs = vars(args) 102 | del kwargs['help'] 103 | seedir(**kwargs) 104 | 105 | if __name__ == '__main__': 106 | main() 107 | 108 | -------------------------------------------------------------------------------- /seedir/printing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | General module of resources and helpers for printing and making folder trees 4 | in seedir. 5 | 6 | """ 7 | 8 | __pdoc__ = {'is_match': False, 9 | 'format_indent': False, 10 | 'words': False} 11 | 12 | 13 | 14 | import copy 15 | import os 16 | import re 17 | 18 | 19 | STYLE_DICT = { 20 | 'lines': {'split':'├─', 21 | 'extend':'│ ', 22 | 'space':' ', 23 | 'final':'└─', 24 | 'folderstart':'', 25 | 'filestart':'', 26 | 'folderend': '/', 27 | 'fileend': ''}, 28 | 'dash': {'split':'|-', 29 | 'extend':'| ', 30 | 'space':' ', 31 | 'final':'|-', 32 | 'folderstart':'', 33 | 'filestart':'', 34 | 'folderend': '/', 35 | 'fileend': ''}, 36 | 'spaces':{'split':' ', 37 | 'extend':' ', 38 | 'space':' ', 39 | 'final':' ', 40 | 'folderstart':'', 41 | 'filestart':'', 42 | 'folderend': '/', 43 | 'fileend': ''}, 44 | 'plus': {'split':'+-', 45 | 'extend':'| ', 46 | 'space':' ', 47 | 'final':'+-', 48 | 'folderstart':'', 49 | 'filestart':'', 50 | 'folderend': '/', 51 | 'fileend': ''}, 52 | 'arrow': {'split':' ', 53 | 'extend':' ', 54 | 'space':' ', 55 | 'final':' ', 56 | 'folderstart':'>', 57 | 'filestart':'>', 58 | 'folderend': '/', 59 | 'fileend': ''} 60 | } 61 | '''"Tokens" used to create folder trees in different styles''' 62 | 63 | try: 64 | import emoji 65 | STYLE_DICT["emoji"] = { 66 | 'split':'├─', 67 | 'extend':'│ ', 68 | 'space':' ', 69 | 'final':'└─', 70 | 'folderstart':emoji.emojize(':file_folder:' + ' '), 71 | 'filestart':emoji.emojize(':page_facing_up:' + ' '), 72 | 'folderend': '/', 73 | 'fileend': '' 74 | } 75 | except ImportError: 76 | pass 77 | 78 | 79 | filepath = os.path.dirname(os.path.abspath(__file__)) 80 | wordpath = os.path.join(filepath, 'words.txt') 81 | with open(wordpath, 'r') as wordfile: 82 | words = [line.strip() for line in wordfile.readlines()] 83 | """List of dictionary words for seedir.fakedir.randomdir()""" 84 | 85 | # functions 86 | 87 | def is_match(pattern, string, regex=True): 88 | '''Function for matching strings using either regular expression 89 | or literal interpretation.''' 90 | if regex: 91 | return bool(re.search(pattern, string)) 92 | else: 93 | return pattern == string 94 | 95 | def get_styleargs(style): 96 | ''' 97 | Return the string tokens associated with different styles for printing 98 | folder trees with `seedir.realdir.seedir()`. 99 | 100 | Parameters 101 | ---------- 102 | style : str 103 | Style name. Current options are `'lines'`, `'spaces'`, `'arrow'`, 104 | `'plus'`, `'dash'`, or `'emoji'`. 105 | 106 | Raises 107 | ------ 108 | ValueError 109 | Style not recognized. 110 | 111 | ImportError 112 | 'emoji' style requested but emoji package not installed. 113 | 114 | Returns 115 | ------- 116 | dict 117 | Dictionary of tokens for the given style. 118 | 119 | ''' 120 | if style not in STYLE_DICT and style == 'emoji': 121 | error_text = 'style "emoji" requires "emoji" to be installed' 122 | error_text += ' (pip install emoji) ' 123 | raise ImportError(error_text) 124 | elif style not in STYLE_DICT: 125 | error_text = 'style "{}" not recognized, must be '.format(style) 126 | error_text += 'lines, spaces, arrow, plus, dash, or emoji' 127 | raise ValueError(error_text) 128 | else: 129 | return copy.deepcopy(STYLE_DICT[style]) 130 | 131 | def format_indent(style_dict, indent=2): 132 | ''' 133 | Format the indent of style tokens, from seedir.STYLE_DICT or returned 134 | by seedir.get_styleargs(). 135 | 136 | Note that as of v0.3.1, the dictionary is modified in place, 137 | rather than a new copy being created. 138 | 139 | Parameters 140 | ---------- 141 | style_dict : dict 142 | Dictionary of style tokens. 143 | indent : int, optional 144 | Number of spaces to indent. The default is 2. With 0, all tokens 145 | become the null string. With 1, all tokens are only the first 146 | character. With 2, the style tokens are returned unedited. When >2, 147 | the final character of each token (excep the file/folder start/end tokens) 148 | are extened n - indent times, to give a string whose 149 | length is equal to indent. 150 | 151 | Returns 152 | ------- 153 | output : dict 154 | New dictionary of edited tokens. 155 | 156 | ''' 157 | indentable = ['split', 'extend', 'space', 'final'] 158 | if indent < 0 or not isinstance(indent, int): 159 | raise ValueError('indent must be a non-negative integer') 160 | elif indent == 0: 161 | for key in indentable: 162 | style_dict[key] = '' 163 | elif indent == 1: 164 | for key in indentable: 165 | style_dict[key] = style_dict[key][0] 166 | elif indent > 2: 167 | extension = indent - 2 168 | for key in indentable: 169 | val = style_dict[key] 170 | style_dict[key] = val + val[-1] * extension 171 | 172 | return None 173 | -------------------------------------------------------------------------------- /docs/templates/search.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Search 7 | 8 | 9 | 28 | 29 | 30 | 31 | 34 |
35 |

36 | 37 |
38 | 41 | 42 | 43 | 44 | 142 | 143 | -------------------------------------------------------------------------------- /docs/templates/pdf.mako: -------------------------------------------------------------------------------- 1 | <% 2 | import re 3 | import pdoc 4 | from pdoc.html_helpers import to_markdown 5 | 6 | def link(dobj: pdoc.Doc): 7 | name = dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') 8 | if isinstance(dobj, pdoc.External): 9 | return name 10 | return f'[{name}](#{dobj.refname} "{dobj.refname}")' 11 | 12 | def _to_md(text, module): 13 | text = to_markdown(text, docformat=docformat, module=module, link=link) 14 | # Setext H2 headings to atx H2 headings 15 | text = re.sub(r'\n(.+)\n-{3,}\n', r'\n## \1\n\n', text) 16 | # Convert admonitions into simpler paragraphs, dedent contents 17 | def paragraph_fmt(m): 18 | sub = re.sub('\n {,4}', '\n', m.group(4)) 19 | return f'{m.group(2)}**{m.group(3)}:** {sub}' 20 | text = re.sub(r'^(?P( *))!!! \w+ \"([^\"]*)\"(.*(?:\n(?P=indent) +.*)*)', 21 | paragraph_fmt, text, flags=re.MULTILINE) 22 | return text 23 | 24 | def subh(text, level=2): 25 | # Deepen heading levels so H2 becomes H4 etc. 26 | return re.sub(r'\n(#+) +(.+)\n', fr'\n{"#" * level}\1 \2\n', text) 27 | %> 28 | 29 | <%def name="title(level, string, id=None)"> 30 | <% id = f' {{#id}}' if id is not None else '' %> 31 | ${('#' * level) + ' ' + string + id} 32 | 33 | 34 | <%def name="funcdef(f)"> 35 | <% 36 | params = f.params(annotate=show_type_annotations) 37 | returns = show_type_annotations and f.return_annotation() or '' 38 | if returns: 39 | returns = ' \N{non-breaking hyphen}> ' + returns 40 | %> 41 | %if params: 42 | > ${f.funcdef()} ${f.name}( 43 | > ${',\n> '.join(params)} 44 | > )${returns} 45 | %else: 46 | > ${f.funcdef()} ${f.name}()${returns} 47 | %endif 48 | 49 | 50 | <%def name="classdef(c)"> 51 | <% params = c.params(annotate=show_type_annotations) %> 52 | %if params: 53 | > class ${c.name}( 54 | > ${',\n> '.join(params)} 55 | > ) 56 | %else: 57 | > class ${c.name} 58 | %endif 59 | 60 | 61 | <%def name="vartype(v)"> 62 | <% annot = show_type_annotations and v.type_annotation() or '' %> 63 | %if annot: 64 | Type: `${annot}` 65 | %endif 66 | 67 | 68 | --- 69 | description: | 70 | API documentation for modules: ${', '.join(m.name for m in modules)}. 71 | 72 | lang: en 73 | 74 | classoption: oneside 75 | geometry: margin=1in 76 | papersize: a4 77 | 78 | linkcolor: blue 79 | links-as-notes: true 80 | ... 81 | % for module in modules: 82 | <% 83 | submodules = module.submodules() 84 | variables = module.variables(sort=sort_identifiers) 85 | functions = module.functions(sort=sort_identifiers) 86 | classes = module.classes(sort=sort_identifiers) 87 | 88 | def to_md(text): 89 | return _to_md(text, module) 90 | %> 91 | ${title(1, ('Namespace' if module.is_namespace else 'Module') + f' `{module.name}`', module.refname)} 92 | ${module.docstring | to_md} 93 | 94 | % if submodules: 95 | ${title(2, 'Sub-modules')} 96 | % for m in submodules: 97 | * [${m.name}](#${m.refname}) 98 | % endfor 99 | % endif 100 | 101 | % if variables: 102 | ${title(2, 'Variables')} 103 | % for v in variables: 104 | ${title(3, f'Variable `{v.name}`', v.refname)} 105 | ${vartype(v)} 106 | ${v.docstring | to_md, subh, subh} 107 | % endfor 108 | % endif 109 | 110 | % if functions: 111 | ${title(2, 'Functions')} 112 | % for f in functions: 113 | ${title(3, f'Function `{f.name}`', f.refname)} 114 | 115 | ${funcdef(f)} 116 | 117 | ${f.docstring | to_md, subh, subh} 118 | % endfor 119 | % endif 120 | 121 | % if classes: 122 | ${title(2, 'Classes')} 123 | % for cls in classes: 124 | ${title(3, f'Class `{cls.name}`', cls.refname)} 125 | 126 | ${classdef(cls)} 127 | 128 | ${cls.docstring | to_md, subh} 129 | <% 130 | class_vars = cls.class_variables(show_inherited_members, sort=sort_identifiers) 131 | static_methods = cls.functions(show_inherited_members, sort=sort_identifiers) 132 | inst_vars = cls.instance_variables(show_inherited_members, sort=sort_identifiers) 133 | methods = cls.methods(show_inherited_members, sort=sort_identifiers) 134 | mro = cls.mro() 135 | subclasses = cls.subclasses() 136 | %> 137 | % if mro: 138 | ${title(4, 'Ancestors (in MRO)')} 139 | % for c in mro: 140 | * [${c.refname}](#${c.refname}) 141 | % endfor 142 | % endif 143 | 144 | % if subclasses: 145 | ${title(4, 'Descendants')} 146 | % for c in subclasses: 147 | * [${c.refname}](#${c.refname}) 148 | % endfor 149 | % endif 150 | 151 | % if class_vars: 152 | ${title(4, 'Class variables')} 153 | % for v in class_vars: 154 | ${title(5, f'Variable `{v.name}`', v.refname)} 155 | ${vartype(v)} 156 | ${v.docstring | to_md, subh, subh} 157 | % endfor 158 | % endif 159 | 160 | % if inst_vars: 161 | ${title(4, 'Instance variables')} 162 | % for v in inst_vars: 163 | ${title(5, f'Variable `{v.name}`', v.refname)} 164 | ${vartype(v)} 165 | ${v.docstring | to_md, subh, subh} 166 | % endfor 167 | % endif 168 | 169 | % if static_methods: 170 | ${title(4, 'Static methods')} 171 | % for f in static_methods: 172 | ${title(5, f'`Method {f.name}`', f.refname)} 173 | 174 | ${funcdef(f)} 175 | 176 | ${f.docstring | to_md, subh, subh} 177 | % endfor 178 | % endif 179 | 180 | % if methods: 181 | ${title(4, 'Methods')} 182 | % for f in methods: 183 | ${title(5, f'Method `{f.name}`', f.refname)} 184 | 185 | ${funcdef(f)} 186 | 187 | ${f.docstring | to_md, subh, subh} 188 | % endfor 189 | % endif 190 | % endfor 191 | % endif 192 | 193 | ##\## for module in modules: 194 | % endfor 195 | 196 | ----- 197 | Generated by *pdoc* ${pdoc.__version__} (). 198 | -------------------------------------------------------------------------------- /docs/seedir/errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | seedir.errors API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module seedir.errors

23 |
24 |
25 |

Custom exceptions for seedir.

26 |
27 | 28 | Expand source code 29 | 30 |
# -*- coding: utf-8 -*-
 31 | """
 32 | Custom exceptions for seedir.
 33 | 
 34 | """
 35 | 
 36 | # NOTE: SeedirError removed for 0.4.0
 37 | 
 38 | # class SeedirError(Exception):
 39 | #     """Class for representing errors from module `seedir.realdir`"""
 40 | 
 41 | class FakedirError(Exception):
 42 |     """Class for handling errors from module `seedir.fakedir`"""
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |

Classes

53 |
54 |
55 | class FakedirError 56 | (*args, **kwargs) 57 |
58 |
59 |

Class for handling errors from module seedir.fakedir

60 |
61 | 62 | Expand source code 63 | 64 |
class FakedirError(Exception):
 65 |     """Class for handling errors from module `seedir.fakedir`"""
66 |
67 |

Ancestors

68 |
    69 |
  • builtins.Exception
  • 70 |
  • builtins.BaseException
  • 71 |
72 |
73 |
74 |
75 |
76 | 96 |
97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This document will serve as a record for past and future changes to seedir. It is being retroactively added after the version [0.2.0](https://github.com/earnestt1234/seedir/releases/tag/v0.2.0) release (so notes for prior releases will be less detailed). 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [0.5.1](https://github.com/earnestt1234/seedir/releases/tag/v0.5.1) 8 | 9 | ### Added 10 | 11 | - GitHub workflows for testing the code (on pushes and pulls to `main` and `dev`) and for uploading to PyPI (on new release). 12 | - Created a pyproject.toml 13 | - Add CONTRIBUTING.md for instructions on how to contribute to the project 14 | 15 | ### Changed 16 | 17 | - `seedir.fakedir.fakedir_fromstring` has been refactored. The logic is essentially the same, but there have been some simplifications and more comments have been added. 18 | 19 | ## [0.5.0](https://github.com/earnestt1234/seedir/releases/tag/v0.5.0) 20 | 21 | ### Added 22 | 23 | - The `itemlimit` now accepts a 2-tuple as an argument, indicating a separate limit for folders and files (respectively). 24 | - Added two parameters for handling errors when trying to list the children of a directory: 25 | - `acceptable_listdir_errors`: One or more error types (`Exceptions`) which are ignored when occurring during a directory listing call. E.g., a permissions error. 26 | - `denied_string`: String to add to follow directory entries for which the error was triggered. 27 | - More test cases added 28 | 29 | ### Changed 30 | 31 | - The main algorithm for folder tree traversal has been refactored. 32 | - The unit tests are now structured for pytest. 33 | - `seedir.folderstructure.FolderStructure` is now an abstract class that cannot be directly instantiated. The functions that previously needed to be provided as arguments for the constructor must now be implemented as part of a subclass (see [getting started for an example](https://earnestt1234.github.io/seedir/seedir/index.html#getting-started)) 34 | 35 | ### Deprecated 36 | 37 | - `slash` is now totally deprecated; use `folderend` instead. 38 | 39 | ## [0.4.2](https://github.com/earnestt1234/seedir/releases/tag/v0.4.2) 40 | 41 | ### Changed 42 | 43 | - Syntax for accessing the CLI is updated. Now, you can do `seedir` or `python -m seedir`; `seedir.command_line` is renamed to enable this change. 44 | 45 | ## [0.4.0](https://github.com/earnestt1234/seedir/releases/tag/v0.4.0) 46 | 47 | ### Added 48 | 49 | - pathlib Path objects now accepted by `sd.seedir()`. All other arguments apply as normal; arguments accepting callables (`mask` and `formatter`) will see pathlib objects. 50 | 51 | ### Changed 52 | 53 | - [emoji is now an optional dependency](https://github.com/earnestt1234/seedir/issues/12). It can be installed with `pip install seedir[emoji]`. An error is raised if the emoji style is requested without emoji installed. 54 | - Reorganization of `folderstructure.py` and the `FolderStructure` class 55 | - `folderstructurehelpers.py` has been removed. Most of the functions implemented there have become methods of `FolderStucture`. 56 | - FolderStructure has been made more user-friendly, and can now be initialized with less functions. 57 | - There are no longer separate "real dir"/"fake dir" functions for handing item filtering/sorting. 58 | 59 | - Item inclusion is now prioritized above exclusion for include/exclude folders/files. The order of precedence now is mask (1), inclusion (2), exclusion (3). The code in this function was generally rewritten to be more concise (`FolderStructure._filter_items()`). 60 | - The `~` in paths is now resolved, as well as `.` and `..` 61 | - More examples in the getting started readme. 62 | 63 | ### Fixed 64 | 65 | - Typos in documentation 66 | - Removal of IPYNB checkpoints 67 | 68 | ### Removed 69 | 70 | - The `SeedirError` has been removed. All uses have been replaced with more appropriate errors, mainly `ValueError` or `TypeError` 71 | 72 | ## [0.3.1](https://github.com/earnestt1234/seedir/releases/tag/v0.3.1) 73 | 74 | ### Added 75 | - Additional functionality to the `formatter` parameter: can now dynamically set other seedir arguments as well as styling ones 76 | - Added a `sticky_formatter` parameter for causing the formatter changes to continue through sub-directories. 77 | - Additional test cases for `formatter` 78 | - Add `folderend` & `fileend` tokens for setting characters at end of line. Default styles have been updated to include these. 79 | - Additional documentation, specifically for `formatter` but also some smaller tweaks 80 | 81 | ### Changed 82 | - The CLI was revamped, now using argparse instead of getopts. More seedir options were added. 83 | - The `seedir.printing.format_indent()` method now modifies dictionaries in place, rather than creating a new one 84 | - the `FakeDir.realize()` method no longer creates a reference to unused file variable 85 | 86 | ### Fixed 87 | - Fix the words.txt file not being closed after opening 🤦 88 | 89 | ### Deprecated 90 | 91 | - `slash` is on warning for removal after addition of `folderend`. 92 | 93 | ## [0.3.0](https://github.com/earnestt1234/seedir/releases/tag/v0.3.0) - 2021-12-20 94 | 95 | ### Added 96 | 97 | - [`formatter` parameter](https://github.com/earnestt1234/seedir/issues/4) for more customizable diagrams 98 | - FakeDir methods for creating files/folders now return references to the objects created 99 | - A `copy()` method for FakeDirs 100 | - A `siblings()` method for FakeDirs 101 | 102 | ### Changed 103 | 104 | - Documentation updates 105 | - Added code blocks to examples in docstrings 106 | - replaced the "cheatsheet.png" image in the Getting Started section with a markdown link 107 | - remove broken link to `FolderStructure` class in `seedir.realdir` module 108 | - fixed some examples in docstrings and readmes 109 | - in getting started, redo examples to omit empty folders (which are omitted by GitHub) 110 | - replace `'\s'` with `' '` in `seedir.fakedir.fakedir_fromstring()` 111 | - [Remove .DS_Store files](https://github.com/earnestt1234/seedir/pull/5) (thanks @[timweissenfels](https://github.com/timweissenfels)) 112 | - Reverted API doc style back to pdoc3 default 113 | 114 | ### Fixed 115 | 116 | - remove [call to `copy` method](https://github.com/earnestt1234/seedir/blob/09fbed86a356fa9b01588546e1e7dbda15812b49/seedir/fakedir.py#L417) in `Fakedir.delete()`, which prevented non-list arguments 117 | - the `walk_apply` method is now applied to the calling folder, rather than just children 118 | 119 | ## [0.2.0](https://github.com/earnestt1234/seedir/releases/tag/v0.2.0) - 2021-05-21 120 | 121 | ### Added 122 | 123 | - a new `folderstructure.py` module was added containing a single folder tree construction algorithm. This module is called for both real directories (`seedir.realdir.seedir()`) and fake directories (`seedir.fakedir.Fakedir.seedir()`). The code was also refactored to be less convoluted and more readable (and also more correct for some fringe cases) 124 | - using `depthlimit=0` with a `beyond` string now produces just the root folder (this was incorrect before) 125 | - with `beyond='content'`, empty folders are only shown when the `depthlimit` is crossed 126 | - trailing separators or slashes no longer cause the name of the root folder to be empty 127 | - A `folderstructurehelpers.py` module was also added to support the main folder structure algorithm. 128 | - More unit tests were added. 129 | - `name` parameter added to `seedir.fakedir.randomdir` 130 | - Convert some arguments to `bool` or `int` in the command line tool 131 | 132 | ### Changed 133 | 134 | - The documentation was overhauled: 135 | - The Jupyter Notebook examples file was removed, in favor of a `gettingstarted.md` file which is included in the main page of the API docs. 136 | - The getting started README as well as the `seedir.fakedir` module are now `doctest`-able. 137 | - New style added to the API docs. 138 | - `seedir.seedir` module has been renamed to `seedir.realdir` to avoid some of my confusions 139 | - added an `exampledir` to the `docs` folder for some examples 140 | - Some changes to `seedir.fakedir.fakedir_fromstring` to handle some more failed cases. 141 | - `words`, `FakeItem` removed from package namespace 142 | 143 | ## [0.1.4](https://github.com/earnestt1234/seedir/releases/tag/v0.1.4) - 2021-03-01 144 | 145 | ### Added 146 | 147 | - Documentation was updated to include code formatting and within-package hyperlinks (thanks to pdoc) 148 | 149 | ### Changed 150 | 151 | - the variable name of the output of `seedir.seedir()` and `seedir.fakedir()` was renamed from `rfs` to `s` 152 | 153 | ## [0.1.3](https://github.com/earnestt1234/seedir/releases/tag/v0.1.3) - 2020-12-31 154 | 155 | ### Changed 156 | 157 | - Code block examples added to docstrings, following [this issue](https://github.com/earnestt1234/seedir/issues/3) 158 | 159 | ## [0.1.2](https://github.com/earnestt1234/seedir/releases/tag/v0.1.2) - 2020-11-28 160 | 161 | ### Added 162 | 163 | - [new `mask` parameter](https://github.com/earnestt1234/seedir/issues/1) for functionally filtering items, used by `seedir.seedir`, `seedir.FakeDir.seedir`, and `seedir.fakedir` 164 | - test cases for the `mask` parameter 165 | 166 | ### Changed 167 | 168 | - Updated examples notebook 169 | - Updated documentation 170 | 171 | ## [0.1.1](https://github.com/earnestt1234/seedir/releases/tag/v0.1.1) - 2020-11-20 172 | 173 | Initial public release of seedir 🎉 -------------------------------------------------------------------------------- /docs/templates/css.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | from pdoc.html_helpers import minify_css 3 | %> 4 | 5 | <%def name="mobile()" filter="minify_css"> 6 | :root { 7 | --highlight-color: #fe9; 8 | } 9 | .flex { 10 | display: flex !important; 11 | } 12 | 13 | body { 14 | line-height: 1.5em; 15 | } 16 | 17 | #content { 18 | padding: 20px; 19 | } 20 | 21 | #sidebar { 22 | padding: 30px; 23 | overflow: hidden; 24 | } 25 | #sidebar > *:last-child { 26 | margin-bottom: 2cm; 27 | } 28 | 29 | % if lunr_search is not None: 30 | #lunr-search { 31 | width: 100%; 32 | font-size: 1em; 33 | padding: 6px 9px 5px 9px; 34 | border: 1px solid silver; 35 | } 36 | % endif 37 | 38 | .http-server-breadcrumbs { 39 | font-size: 130%; 40 | margin: 0 0 15px 0; 41 | } 42 | 43 | #footer { 44 | font-size: .75em; 45 | padding: 5px 30px; 46 | border-top: 1px solid #ddd; 47 | text-align: right; 48 | } 49 | #footer p { 50 | margin: 0 0 0 1em; 51 | display: inline-block; 52 | } 53 | #footer p:last-child { 54 | margin-right: 30px; 55 | } 56 | 57 | h1, h2, h3, h4, h5 { 58 | font-weight: 300; 59 | } 60 | h1 { 61 | font-size: 2.5em; 62 | line-height: 1.1em; 63 | } 64 | h2 { 65 | font-size: 1.75em; 66 | margin: 1em 0 .50em 0; 67 | } 68 | h3 { 69 | font-size: 1.4em; 70 | margin: 25px 0 10px 0; 71 | } 72 | h4 { 73 | margin: 0; 74 | font-size: 105%; 75 | } 76 | h1:target, 77 | h2:target, 78 | h3:target, 79 | h4:target, 80 | h5:target, 81 | h6:target { 82 | background: var(--highlight-color); 83 | padding: .2em 0; 84 | } 85 | 86 | a { 87 | color: #058; 88 | text-decoration: none; 89 | transition: color .3s ease-in-out; 90 | } 91 | a:hover { 92 | color: #e82; 93 | } 94 | 95 | .title code { 96 | font-weight: bold; 97 | } 98 | h2[id^="header-"] { 99 | margin-top: 2em; 100 | } 101 | .ident { 102 | color: #900; 103 | } 104 | 105 | pre code { 106 | background: #f8f8f8; 107 | font-size: .8em; 108 | line-height: 1.15em; 109 | } 110 | code { 111 | background: #f2f2f1; 112 | padding: 1px 4px; 113 | overflow-wrap: break-word; 114 | } 115 | h1 code { background: transparent } 116 | 117 | pre { 118 | background: #f8f8f8; 119 | border: 0; 120 | border-top: 1px solid #ccc; 121 | border-bottom: 1px solid #ccc; 122 | margin: 1em 0; 123 | padding: 1ex; 124 | } 125 | 126 | #http-server-module-list { 127 | display: flex; 128 | flex-flow: column; 129 | } 130 | #http-server-module-list div { 131 | display: flex; 132 | } 133 | #http-server-module-list dt { 134 | min-width: 10%; 135 | } 136 | #http-server-module-list p { 137 | margin-top: 0; 138 | } 139 | 140 | .toc ul, 141 | #index { 142 | list-style-type: none; 143 | margin: 0; 144 | padding: 0; 145 | } 146 | #index code { 147 | background: transparent; 148 | } 149 | #index h3 { 150 | border-bottom: 1px solid #ddd; 151 | } 152 | #index ul { 153 | padding: 0; 154 | } 155 | #index h4 { 156 | margin-top: .6em; 157 | font-weight: bold; 158 | } 159 | /* Make TOC lists have 2+ columns when viewport is wide enough. 160 | Assuming ~20-character identifiers and ~30% wide sidebar. */ 161 | @media (min-width: 200ex) { #index .two-column { column-count: 2 } } 162 | @media (min-width: 300ex) { #index .two-column { column-count: 3 } } 163 | 164 | dl { 165 | margin-bottom: 2em; 166 | } 167 | dl dl:last-child { 168 | margin-bottom: 4em; 169 | } 170 | dd { 171 | margin: 0 0 1em 3em; 172 | } 173 | #header-classes + dl > dd { 174 | margin-bottom: 3em; 175 | } 176 | dd dd { 177 | margin-left: 2em; 178 | } 179 | dd p { 180 | margin: 10px 0; 181 | } 182 | .name { 183 | background: #eee; 184 | font-weight: bold; 185 | font-size: .85em; 186 | padding: 5px 10px; 187 | display: inline-block; 188 | min-width: 40%; 189 | } 190 | .name:hover { 191 | background: #e0e0e0; 192 | } 193 | dt:target .name { 194 | background: var(--highlight-color); 195 | } 196 | .name > span:first-child { 197 | white-space: nowrap; 198 | } 199 | .name.class > span:nth-child(2) { 200 | margin-left: .4em; 201 | } 202 | .inherited { 203 | color: #999; 204 | border-left: 5px solid #eee; 205 | padding-left: 1em; 206 | } 207 | .inheritance em { 208 | font-style: normal; 209 | font-weight: bold; 210 | } 211 | 212 | /* Docstrings titles, e.g. in numpydoc format */ 213 | .desc h2 { 214 | font-weight: 400; 215 | font-size: 1.25em; 216 | } 217 | .desc h3 { 218 | font-size: 1em; 219 | } 220 | .desc dt code { 221 | background: inherit; /* Don't grey-back parameters */ 222 | } 223 | 224 | .source summary, 225 | .git-link-div { 226 | color: #666; 227 | text-align: right; 228 | font-weight: 400; 229 | font-size: .8em; 230 | text-transform: uppercase; 231 | } 232 | .source summary > * { 233 | white-space: nowrap; 234 | cursor: pointer; 235 | } 236 | .git-link { 237 | color: inherit; 238 | margin-left: 1em; 239 | } 240 | .source pre { 241 | max-height: 500px; 242 | overflow: auto; 243 | margin: 0; 244 | } 245 | .source pre code { 246 | font-size: 12px; 247 | overflow: visible; 248 | } 249 | .hlist { 250 | list-style: none; 251 | } 252 | .hlist li { 253 | display: inline; 254 | } 255 | .hlist li:after { 256 | content: ',\2002'; 257 | } 258 | .hlist li:last-child:after { 259 | content: none; 260 | } 261 | .hlist .hlist { 262 | display: inline; 263 | padding-left: 1em; 264 | } 265 | 266 | img { 267 | max-width: 100%; 268 | } 269 | td { 270 | padding: 0 .5em; 271 | } 272 | 273 | .admonition { 274 | padding: .1em .5em; 275 | margin-bottom: 1em; 276 | } 277 | .admonition-title { 278 | font-weight: bold; 279 | } 280 | .admonition.note, 281 | .admonition.info, 282 | .admonition.important { 283 | background: #aef; 284 | } 285 | .admonition.todo, 286 | .admonition.versionadded, 287 | .admonition.tip, 288 | .admonition.hint { 289 | background: #dfd; 290 | } 291 | .admonition.warning, 292 | .admonition.versionchanged, 293 | .admonition.deprecated { 294 | background: #fd4; 295 | } 296 | .admonition.error, 297 | .admonition.danger, 298 | .admonition.caution { 299 | background: lightpink; 300 | } 301 | 302 | 303 | <%def name="desktop()" filter="minify_css"> 304 | @media screen and (min-width: 700px) { 305 | #sidebar { 306 | width: 30%; 307 | height: 100vh; 308 | overflow: auto; 309 | position: sticky; 310 | top: 0; 311 | } 312 | #content { 313 | width: 70%; 314 | max-width: 100ch; 315 | padding: 3em 4em; 316 | border-left: 1px solid #ddd; 317 | } 318 | pre code { 319 | font-size: 1em; 320 | } 321 | .item .name { 322 | font-size: 1em; 323 | } 324 | main { 325 | display: flex; 326 | flex-direction: row-reverse; 327 | justify-content: flex-end; 328 | } 329 | .toc ul ul, 330 | #index ul { 331 | padding-left: 1.5em; 332 | } 333 | .toc > ul > li { 334 | margin-top: .5em; 335 | } 336 | } 337 | 338 | 339 | <%def name="print()" filter="minify_css"> 340 | @media print { 341 | #sidebar h1 { 342 | page-break-before: always; 343 | } 344 | .source { 345 | display: none; 346 | } 347 | } 348 | @media print { 349 | * { 350 | background: transparent !important; 351 | color: #000 !important; /* Black prints faster: h5bp.com/s */ 352 | box-shadow: none !important; 353 | text-shadow: none !important; 354 | } 355 | 356 | a[href]:after { 357 | content: " (" attr(href) ")"; 358 | font-size: 90%; 359 | } 360 | /* Internal, documentation links, recognized by having a title, 361 | don't need the URL explicity stated. */ 362 | a[href][title]:after { 363 | content: none; 364 | } 365 | 366 | abbr[title]:after { 367 | content: " (" attr(title) ")"; 368 | } 369 | 370 | /* 371 | * Don't show links for images, or javascript/internal links 372 | */ 373 | 374 | .ir a:after, 375 | a[href^="javascript:"]:after, 376 | a[href^="#"]:after { 377 | content: ""; 378 | } 379 | 380 | pre, 381 | blockquote { 382 | border: 1px solid #999; 383 | page-break-inside: avoid; 384 | } 385 | 386 | thead { 387 | display: table-header-group; /* h5bp.com/t */ 388 | } 389 | 390 | tr, 391 | img { 392 | page-break-inside: avoid; 393 | } 394 | 395 | img { 396 | max-width: 100% !important; 397 | } 398 | 399 | @page { 400 | margin: 0.5cm; 401 | } 402 | 403 | p, 404 | h2, 405 | h3 { 406 | orphans: 3; 407 | widows: 3; 408 | } 409 | 410 | h1, 411 | h2, 412 | h3, 413 | h4, 414 | h5, 415 | h6 { 416 | page-break-after: avoid; 417 | } 418 | } 419 | 420 | -------------------------------------------------------------------------------- /seedir/realdir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Note: this module fails doctesting 4 | 5 | """ 6 | This module provides code for producing folder structure strings for directories. 7 | Currently, the only tool here is `seedir()`, the primary function of the 8 | package `seedir`. This returns or prints the folder structure for a given path. 9 | The main algorithm for determining the folder structure string is within the 10 | seedir.folderstructure.FolderStructure class. 11 | """ 12 | 13 | import os 14 | import pathlib 15 | 16 | from seedir.folderstructure import PathlibStructure, RealDirStructure 17 | 18 | def _parse_path(path): 19 | '''Helper function added to parse the input to `seedir.realdir.seedir()`. 20 | Detects strings (paths) or pathlib objects.''' 21 | 22 | if isinstance(path, str): 23 | path = os.path.abspath(os.path.expanduser(path)) 24 | elif isinstance(path, pathlib.Path): 25 | path = path.expanduser().resolve() 26 | else: 27 | raise TypeError(f"Can only parse str or pathlib.Path, not {type(path)}.") 28 | 29 | return path 30 | 31 | def seedir(path=None, style='lines', printout=True, indent=2, uniform=None, 32 | anystart=None, anyend=None, depthlimit=None, itemlimit=None, 33 | beyond=None, first=None, sort=False, sort_reverse=False, 34 | sort_key=None, include_folders=None, exclude_folders=None, 35 | include_files=None, exclude_files=None, regex=False, mask=None, 36 | formatter=None, sticky_formatter=False, 37 | acceptable_listdir_errors=PermissionError, 38 | denied_string=' [ACCESS DENIED]', **kwargs): 39 | ''' 40 | 41 | Primary function of the seedir package: generate folder trees for 42 | computer directories. 43 | 44 | ## EXAMPLES 45 | 46 | ``` 47 | >>> import seedir as sd 48 | 49 | ``` 50 | 51 | Make a basic tree diagram: 52 | 53 | ``` 54 | >>> c = 'path/to/doc' 55 | >>> sd.seedir(c) 56 | doc/ 57 | ├─_static/ 58 | │ ├─embedded/ 59 | │ │ ├─deep_file 60 | │ │ └─very/ 61 | │ │ └─deep/ 62 | │ │ └─folder/ 63 | │ │ └─very_deep_file 64 | │ └─less_deep_file 65 | ├─about.rst 66 | ├─conf.py 67 | └─index.rst 68 | 69 | ``` 70 | 71 | Select different styles for the tree: 72 | 73 | ``` 74 | >>> sd.seedir(c, style='dash') 75 | doc/ 76 | |-_static/ 77 | | |-embedded/ 78 | | | |-deep_file 79 | | | |-very/ 80 | | | |-deep/ 81 | | | |-folder/ 82 | | | |-very_deep_file 83 | | |-less_deep_file 84 | |-about.rst 85 | |-conf.py 86 | |-index.rst 87 | 88 | ``` 89 | 90 | Sort the folder contents, separting folders and files: 91 | 92 | ``` 93 | >>> sd.seedir(c, sort=True, first='files') 94 | doc/ 95 | ├─about.rst 96 | ├─conf.py 97 | ├─index.rst 98 | └─_static/ 99 | ├─less_deep_file 100 | └─embedded/ 101 | ├─deep_file 102 | └─very/ 103 | └─deep/ 104 | └─folder/ 105 | └─very_deep_file 106 | 107 | ``` 108 | 109 | Limit the folder depth or items included: 110 | 111 | ``` 112 | >>> sd.seedir(c, depthlimit=2, itemlimit=1) 113 | doc/ 114 | ├─_static/ 115 | │ ├─embedded/ 116 | │ └─less_deep_file 117 | └─about.rst 118 | 119 | ``` 120 | 121 | Include or exclude specific items (with or without regular expressions): 122 | 123 | ``` 124 | >>> sd.seedir(c, exclude_folders='_static') 125 | doc/ 126 | ├─about.rst 127 | ├─conf.py 128 | └─index.rst 129 | 130 | ``` 131 | 132 | Parameters 133 | ---------- 134 | path : str, pathlib.Path, or None, optional 135 | System path of a directory. If None, current working directory is 136 | used. The path can be either a string path or a pathlib object. 137 | In both cases, the path is converted to an absolute path, and the 138 | tilde (~) is expanded. 139 | style : 'lines', 'dash', 'arrow', 'spaces', 'plus', or 'emoji', optional 140 | Style to use. The default is `'lines'`. A style determines the set 141 | of characters ("tokens") used to represent the base structure of 142 | the directory (e.g. which items belong to which folders, when items 143 | are the last member of a folder, etc.). The actual tokens being used 144 | by each style can be viewed with `seedir.printing.get_styleargs()`. 145 | printout : bool, optional 146 | Print the folder structure in the console. The default is `True`. When 147 | `False`, the folder diagram is returned as a string. 148 | indent : int (>= 0), optional 149 | Number of spaces separating items from their parent folder. 150 | The default is `2`. 151 | uniform : str or None, optional 152 | Characters to use for all tokens when creating the tree diagram. 153 | The default is `None`. When not `None`, the extend, space, split, and 154 | final tokens are replaced with `uniform` (the `'spaces'` style is 155 | essentially `uniform = ' '`). 156 | anystart : str or None, optional 157 | Characters to prepend before any item (i.e. folder or file). The 158 | default is `None`. Specific starts for folders and files can be 159 | specified (see `**kwargs`). 160 | anyend : str or None, optional 161 | Characters to append after any item (i.e. folder or file). The 162 | default is `None`. Specific ends for folders and files can be 163 | specified (see `**kwargs`). 164 | depthlimit : int or None, optional 165 | Limit the depth of folders to traverse. Folders at the `depthlimit` are 166 | included, but their contents are not shown (with the exception of the 167 | beyond parameter being specified). The default is `None`, which can 168 | cause exceptionally long runtimes for deep or extensive directories. 169 | itemlimit : int or None, optional 170 | Limit the number of items in a directory to show. Items beyond the 171 | `itemlimit` can be expressed using the `beyond` parameter. The files and 172 | folders left out are determined by the sorting parameters 173 | (`sort`, `sort_reverse`, `sort_key`). The default is `None`. 174 | beyond : str ('ellipsis', 'content' or a string starting with an underscore) or None, optional 175 | String to indicate directory contents beyond the `itemlimit` or the 176 | `depthlimit`. The default is `None`. Options are: `'ellipsis'` (`'...'`), 177 | `'content'` or `'contents'` (the number of files and folders beyond), or 178 | a string starting with `'_'` (everything after the leading underscore 179 | will be returned) 180 | first : 'files', 'folders', or None, optional 181 | Sort the directory so that either files or folders appear first. 182 | The default is `None`. 183 | sort : bool, optional 184 | Sort the directory. With no other specifications, the sort will be a 185 | simple alphabetical sort of the item names, but this can be altered 186 | with the `first`, `sort_reverse`, and `sort_key parameters`. 187 | The default is `False`. 188 | sort_reverse : bool, optional 189 | Reverse the sorting determined by `sort` or `sort_key`. 190 | The default is `False`. 191 | sort_key : function, optional 192 | Key to use for sorting file or folder names, akin to the `key` parameter 193 | of the builtin `sorted()` or `list.sort()`. The function should take a 194 | string as an argument. The default is `None`. 195 | include_folders, exclude_folders, include_files, exclude_files : str, list-like, or None, optional 196 | Folder / file names to include or exclude. The default is `None`. By 197 | default, these are interpreted literally. Pass `regex=True` for 198 | using regular expressions. 199 | regex : bool, optional 200 | Interpret the strings of include/exclude file/folder arguments as 201 | regular expressions. The default is `False`. 202 | mask : function, optional 203 | Function for filtering items. Absolute paths of each individual item 204 | are passed to the `mask` function. If `True` is returned, the 205 | item is kept. The default is `None`. The type of the object 206 | passed to `mask` corresponds with that passed as input: 207 | `str` or `pathlib.Path`. 208 | formatter : function, optional 209 | Function for customizing the directory printing logic and style 210 | based on specific folders & files. When passed, the formatter 211 | is called on each item in the file tree, and the current arguments 212 | are updated based what is returned. 213 | 214 | The formatter function should accept a system path as a 215 | single argument (either relative or absolute, depending on what is passed 216 | to the `path` argument), and it should return either a dictionary or None. 217 | The dictionary should have names of arguments as keys and their respective 218 | setting as values. 219 | 220 | The following options can meaningfully be toggled by passing a formatter 221 | function: `depthlimit`, `itemlimit`, `beyond`, `first`, `sort`, `sort_reverse`, 222 | `sort_key`, `include_folders`, `regex`, `mask`, as well as any seedir token 223 | keywords (`extend`, `space`, `split`, `final`, `folderstart`, `filestart`, 224 | `folderend`, `fileend`). 225 | 226 | Note that in version 0.3.0, formatter could only be used to update 227 | the style tokens. It can now be used to udpate those as well as the other 228 | arguments listed above. 229 | 230 | If None is returned by formatter, the tokens will be set by `style`. 231 | 232 | Note that items exlcuded by the inclusion/exclusion arguments (or the 233 | `mask`) *will not* be seen by formatter. Similarly, any folder tree 234 | entries created by the `beyond` argument *will not* be seen by formatter. 235 | 236 | The type of the object passed to `formatter` corresponds with that 237 | passed as input: `str` or `pathlib.Path`. 238 | 239 | sticky_formatter : bool, optional 240 | When True, updates to argumnts made by the `formatter` (see above) 241 | will be permanent. Thus, if arguments are updated when the `formatter` 242 | is called on a folder, its children will (recursively) inherit 243 | those new arguments. 244 | acceptable_listdir_errors : Exception, tuple, or None 245 | 246 | **New in v0.5.0** 247 | 248 | Set errors which, when raised when listing the contents of a folder, 249 | do not halt traversal. This parameter was added to allow 250 | folders to be skipped when a `PermissionError` is raised. For folders 251 | which raise an acceptable error, an additional string is added to their 252 | entry in the diagram (see `denied_string`). 253 | 254 | Providing one Exception causes only that type of Exception to be ignored. 255 | Multiple Exception types can be handled by passing mutple Exceptions 256 | as a tuple. Pass `None` to not allow any Exceptions (in this case, 257 | an error is raised). 258 | 259 | Exception types which are not provided will still be raised. 260 | 261 | The default is `PermissionError`, which will skip folders 262 | for which the caller does not have permissions to access. 263 | 264 | denied_string : str 265 | 266 | String tag to signify that a folder was not able to be traversed 267 | due to one of the `acceptable_listdir_errors` being raised. This 268 | is a string added after the folder name (and `folderend`) strings. 269 | The default is `" [ACCESS DENIED]"`. 270 | 271 | **kwargs : str 272 | Specific tokens to use for creating the file tree diagram. The tokens 273 | use by each builtin style can be seen with `seedir.printing.get_styleargs()`. 274 | Valid options are `extend` (characters to show the extension of a directory 275 | while its children are traversed), `space` (character to provide the 276 | correct indentation of an item when some of its parent / grandparent 277 | directories are completely traversed), `split` (characters to show a 278 | folder or file within a directory, with more items following), 279 | `final` (characters to show a folder or file within a directory, 280 | with no more items following), `folderstart` (characters to prepend 281 | before any folder), `filestart` (characters to preppend before any 282 | file), `folderend` (characters to append after any folder), and 283 | `fileend` (characters to append after any file). The following shows 284 | the default tokens for the `'lines'` style: 285 | 286 | >>> import seedir as sd 287 | >>> sd.get_styleargs('lines') 288 | {'split': '├─', 'extend': '│ ', 'space': ' ', 'final': '└─', 'folderstart': '', 'filestart': '', 'folderend': '/', 'fileend': ''} 289 | 290 | All default style tokens are 2 character strings, except for 291 | the file/folder start/end tokens. Style tokens from `**kwargs` are not 292 | affected by the indent parameter. The `uniform`, `anystart`, and 293 | `anyend` parameters can be used to affect multiple style tokens. 294 | 295 | Notes 296 | ------- 297 | 298 | The parameter `slash` was deprecated in 0.5.0. Pass `folderend` as 299 | an additional keyword argument instead. 300 | 301 | Returns 302 | ------- 303 | s (str) or None 304 | The tree diagram (as a string) or `None` if `prinout = True`, in which 305 | case the tree diagram is printed in the console. 306 | 307 | ''' 308 | 309 | if path is None: 310 | path = os.getcwd() 311 | 312 | path = _parse_path(path) 313 | 314 | # call 315 | args = dict(style=style, 316 | printout=printout, 317 | indent=indent, 318 | uniform=uniform, 319 | anystart=anystart, 320 | anyend=anyend, 321 | depthlimit=depthlimit, 322 | itemlimit=itemlimit, 323 | beyond=beyond, 324 | first=first, 325 | sort=sort, 326 | sort_reverse=sort_reverse, 327 | sort_key=sort_key, 328 | include_folders=include_folders, 329 | exclude_folders=exclude_folders, 330 | include_files=include_files, 331 | exclude_files=exclude_files, 332 | regex=regex, 333 | mask=mask, 334 | formatter=formatter, 335 | sticky_formatter=sticky_formatter, 336 | acceptable_listdir_errors=acceptable_listdir_errors, 337 | denied_string=denied_string, 338 | **kwargs) 339 | 340 | structure = PathlibStructure() if isinstance(path, pathlib.Path) else RealDirStructure() 341 | 342 | return structure(path, **args) 343 | -------------------------------------------------------------------------------- /docs/seedir/printing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | seedir.printing API documentation 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Module seedir.printing

24 |
25 |
26 |

General module of resources and helpers for printing and making folder trees 27 | in seedir.

28 |
29 | 30 | Expand source code 31 | 32 |
# -*- coding: utf-8 -*-
 33 | """
 34 | General module of resources and helpers for printing and making folder trees
 35 | in seedir.
 36 | 
 37 | """
 38 | 
 39 | __pdoc__ = {'is_match': False,
 40 |             'format_indent': False,
 41 |             'words': False}
 42 | 
 43 | 
 44 | 
 45 | import copy
 46 | import os
 47 | import re
 48 | 
 49 | 
 50 | STYLE_DICT = {
 51 |     'lines': {'split':'├─',
 52 |               'extend':'│ ',
 53 |               'space':'  ',
 54 |               'final':'└─',
 55 |               'folderstart':'',
 56 |               'filestart':'',
 57 |               'folderend': '/',
 58 |               'fileend': ''},
 59 |     'dash':  {'split':'|-',
 60 |               'extend':'| ',
 61 |               'space':'  ',
 62 |               'final':'|-',
 63 |               'folderstart':'',
 64 |               'filestart':'',
 65 |               'folderend': '/',
 66 |               'fileend': ''},
 67 |     'spaces':{'split':'  ',
 68 |               'extend':'  ',
 69 |               'space':'  ',
 70 |               'final':'  ',
 71 |               'folderstart':'',
 72 |               'filestart':'',
 73 |               'folderend': '/',
 74 |               'fileend': ''},
 75 |     'plus':  {'split':'+-',
 76 |               'extend':'| ',
 77 |               'space':'  ',
 78 |               'final':'+-',
 79 |               'folderstart':'',
 80 |               'filestart':'',
 81 |               'folderend': '/',
 82 |               'fileend': ''},
 83 |     'arrow': {'split':'  ',
 84 |               'extend':'  ',
 85 |               'space':'  ',
 86 |               'final':'  ',
 87 |               'folderstart':'>',
 88 |               'filestart':'>',
 89 |               'folderend': '/',
 90 |               'fileend': ''}
 91 |     }
 92 | '''"Tokens" used to create folder trees in different styles'''
 93 | 
 94 | try:
 95 |     import emoji
 96 |     STYLE_DICT["emoji"] = {
 97 |         'split':'├─',
 98 |         'extend':'│ ',
 99 |         'space':'  ',
100 |         'final':'└─',
101 |         'folderstart':emoji.emojize(':file_folder:' + ' '),
102 |         'filestart':emoji.emojize(':page_facing_up:' + ' '),
103 |         'folderend': '/',
104 |         'fileend': ''
105 |     }
106 | except ImportError:
107 |     pass
108 | 
109 | 
110 | filepath = os.path.dirname(os.path.abspath(__file__))
111 | wordpath = os.path.join(filepath, 'words.txt')
112 | with open(wordpath, 'r') as wordfile:
113 |     words = [line.strip() for line in wordfile.readlines()]
114 | """List of dictionary words for seedir.fakedir.randomdir()"""
115 | 
116 | # functions
117 | 
118 | def is_match(pattern, string, regex=True):
119 |     '''Function for matching strings using either regular expression
120 |     or literal interpretation.'''
121 |     if regex:
122 |         return bool(re.search(pattern, string))
123 |     else:
124 |         return pattern == string
125 | 
126 | def get_styleargs(style):
127 |     '''
128 |     Return the string tokens associated with different styles for printing
129 |     folder trees with `seedir.realdir.seedir()`.
130 | 
131 |     Parameters
132 |     ----------
133 |     style : str
134 |         Style name.  Current options are `'lines'`, `'spaces'`, `'arrow'`,
135 |         `'plus'`, `'dash'`, or `'emoji'`.
136 | 
137 |     Raises
138 |     ------
139 |     ValueError
140 |         Style not recognized.
141 | 
142 |     ImportError
143 |         'emoji' style requested but emoji package not installed.
144 | 
145 |     Returns
146 |     -------
147 |     dict
148 |         Dictionary of tokens for the given style.
149 | 
150 |     '''
151 |     if style not in STYLE_DICT and style == 'emoji':
152 |         error_text = 'style "emoji" requires "emoji" to be installed'
153 |         error_text += ' (pip install emoji) '
154 |         raise ImportError(error_text)
155 |     elif style not in STYLE_DICT:
156 |         error_text = 'style "{}" not recognized, must be '.format(style)
157 |         error_text += 'lines, spaces, arrow, plus, dash, or emoji'
158 |         raise ValueError(error_text)
159 |     else:
160 |         return copy.deepcopy(STYLE_DICT[style])
161 | 
162 | def format_indent(style_dict, indent=2):
163 |     '''
164 |     Format the indent of style tokens, from seedir.STYLE_DICT or returned
165 |     by seedir.get_styleargs().
166 | 
167 |     Note that as of v0.3.1, the dictionary is modified in place,
168 |     rather than a new copy being created.
169 | 
170 |     Parameters
171 |     ----------
172 |     style_dict : dict
173 |         Dictionary of style tokens.
174 |     indent : int, optional
175 |         Number of spaces to indent. The default is 2.  With 0, all tokens
176 |         become the null string.  With 1, all tokens are only the first
177 |         character.  With 2, the style tokens are returned unedited.  When >2,
178 |         the final character of each token (excep the file/folder start/end tokens)
179 |         are extened n - indent times, to give a string whose
180 |         length is equal to indent.
181 | 
182 |     Returns
183 |     -------
184 |     output : dict
185 |         New dictionary of edited tokens.
186 | 
187 |     '''
188 |     indentable = ['split', 'extend', 'space', 'final']
189 |     if indent < 0 or not isinstance(indent, int):
190 |         raise ValueError('indent must be a non-negative integer')
191 |     elif indent == 0:
192 |         for key in indentable:
193 |             style_dict[key] = ''
194 |     elif indent == 1:
195 |         for key in indentable:
196 |             style_dict[key] = style_dict[key][0]
197 |     elif indent > 2:
198 |         extension = indent - 2
199 |         for key in indentable:
200 |             val = style_dict[key]
201 |             style_dict[key] = val + val[-1] * extension
202 | 
203 |     return None
204 |
205 |
206 |
207 |
208 |
209 |

Global variables

210 |
211 |
var STYLE_DICT
212 |
213 |

"Tokens" used to create folder trees in different styles

214 |
215 |
216 |
217 |
218 |

Functions

219 |
220 |
221 | def get_styleargs(style) 222 |
223 |
224 |

Return the string tokens associated with different styles for printing 225 | folder trees with seedir().

226 |

Parameters

227 |
228 |
style : str
229 |
Style name. 230 | Current options are 'lines', 'spaces', 'arrow', 231 | 'plus', 'dash', or 'emoji'.
232 |
233 |

Raises

234 |
235 |
ValueError
236 |
Style not recognized.
237 |
ImportError
238 |
'emoji' style requested but emoji package not installed.
239 |
240 |

Returns

241 |
242 |
dict
243 |
Dictionary of tokens for the given style.
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 | 274 |
275 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /docs/templates/html.mako: -------------------------------------------------------------------------------- 1 | <% 2 | import os 3 | 4 | import pdoc 5 | from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link 6 | 7 | 8 | def link(dobj: pdoc.Doc, name=None): 9 | name = name or dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') 10 | if isinstance(dobj, pdoc.External) and not external_links: 11 | return name 12 | url = dobj.url(relative_to=module, link_prefix=link_prefix, 13 | top_ancestor=not show_inherited_members) 14 | return f'{name}' 15 | 16 | 17 | def to_html(text): 18 | return _to_html(text, docformat=docformat, module=module, link=link, latex_math=latex_math) 19 | 20 | 21 | def get_annotation(bound_method, sep=':'): 22 | annot = show_type_annotations and bound_method(link=link) or '' 23 | if annot: 24 | annot = ' ' + sep + '\N{NBSP}' + annot 25 | return annot 26 | %> 27 | 28 | <%def name="ident(name)">${name} 29 | 30 | <%def name="show_source(d)"> 31 | % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): 32 | <% git_link = format_git_link(git_link_template, d) %> 33 | % if show_source_code: 34 |
35 | 36 | Expand source code 37 | % if git_link: 38 | Browse git 39 | %endif 40 | 41 |
${d.source | h}
42 |
43 | % elif git_link: 44 | 45 | %endif 46 | %endif 47 | 48 | 49 | <%def name="show_desc(d, short=False)"> 50 | <% 51 | inherits = ' inherited' if d.inherits else '' 52 | docstring = glimpse(d.docstring) if short or inherits else d.docstring 53 | %> 54 | % if d.inherits: 55 |

56 | Inherited from: 57 | % if hasattr(d.inherits, 'cls'): 58 | ${link(d.inherits.cls)}.${link(d.inherits, d.name)} 59 | % else: 60 | ${link(d.inherits)} 61 | % endif 62 |

63 | % endif 64 |
${docstring | to_html}
65 | % if not isinstance(d, pdoc.Module): 66 | ${show_source(d)} 67 | % endif 68 | 69 | 70 | <%def name="show_module_list(modules)"> 71 |

Python module list

72 | 73 | % if not modules: 74 |

No modules found.

75 | % else: 76 |
77 | % for name, desc in modules: 78 |
79 |
${name}
80 |
${desc | glimpse, to_html}
81 |
82 | % endfor 83 |
84 | % endif 85 | 86 | 87 | <%def name="show_column_list(items)"> 88 | <% 89 | two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) 90 | %> 91 | 96 | 97 | 98 | <%def name="show_module(module)"> 99 | <% 100 | variables = module.variables(sort=sort_identifiers) 101 | classes = module.classes(sort=sort_identifiers) 102 | functions = module.functions(sort=sort_identifiers) 103 | submodules = module.submodules() 104 | %> 105 | 106 | <%def name="show_func(f)"> 107 |
108 | <% 109 | params = ', '.join(f.params(annotate=show_type_annotations, link=link)) 110 | return_type = get_annotation(f.return_annotation, '\N{non-breaking hyphen}>') 111 | %> 112 | ${f.funcdef()} ${ident(f.name)}(${params})${return_type} 113 |
114 |
${show_desc(f)}
115 | 116 | 117 |
118 | % if http_server: 119 | 127 | % endif 128 |

${'Namespace' if module.is_namespace else \ 129 | 'Package' if module.is_package and not module.supermodule else \ 130 | 'Module'} ${module.name}

131 |
132 | 133 |
134 | ${module.docstring | to_html} 135 | ${show_source(module)} 136 |
137 | 138 |
139 | % if submodules: 140 |

Sub-modules

141 |
142 | % for m in submodules: 143 |
${link(m)}
144 |
${show_desc(m, short=True)}
145 | % endfor 146 |
147 | % endif 148 |
149 | 150 |
151 | % if variables: 152 |

Global variables

153 |
154 | % for v in variables: 155 | <% return_type = get_annotation(v.type_annotation) %> 156 |
var ${ident(v.name)}${return_type}
157 |
${show_desc(v)}
158 | % endfor 159 |
160 | % endif 161 |
162 | 163 |
164 | % if functions: 165 |

Functions

166 |
167 | % for f in functions: 168 | ${show_func(f)} 169 | % endfor 170 |
171 | % endif 172 |
173 | 174 |
175 | % if classes: 176 |

Classes

177 |
178 | % for c in classes: 179 | <% 180 | class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) 181 | smethods = c.functions(show_inherited_members, sort=sort_identifiers) 182 | inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) 183 | methods = c.methods(show_inherited_members, sort=sort_identifiers) 184 | mro = c.mro() 185 | subclasses = c.subclasses() 186 | params = ', '.join(c.params(annotate=show_type_annotations, link=link)) 187 | %> 188 |
189 | class ${ident(c.name)} 190 | % if params: 191 | (${params}) 192 | % endif 193 |
194 | 195 |
${show_desc(c)} 196 | 197 | % if mro: 198 |

Ancestors

199 |
    200 | % for cls in mro: 201 |
  • ${link(cls)}
  • 202 | % endfor 203 |
204 | %endif 205 | 206 | % if subclasses: 207 |

Subclasses

208 |
    209 | % for sub in subclasses: 210 |
  • ${link(sub)}
  • 211 | % endfor 212 |
213 | % endif 214 | % if class_vars: 215 |

Class variables

216 |
217 | % for v in class_vars: 218 | <% return_type = get_annotation(v.type_annotation) %> 219 |
var ${ident(v.name)}${return_type}
220 |
${show_desc(v)}
221 | % endfor 222 |
223 | % endif 224 | % if smethods: 225 |

Static methods

226 |
227 | % for f in smethods: 228 | ${show_func(f)} 229 | % endfor 230 |
231 | % endif 232 | % if inst_vars: 233 |

Instance variables

234 |
235 | % for v in inst_vars: 236 | <% return_type = get_annotation(v.type_annotation) %> 237 |
var ${ident(v.name)}${return_type}
238 |
${show_desc(v)}
239 | % endfor 240 |
241 | % endif 242 | % if methods: 243 |

Methods

244 |
245 | % for f in methods: 246 | ${show_func(f)} 247 | % endfor 248 |
249 | % endif 250 | 251 | % if not show_inherited_members: 252 | <% 253 | members = c.inherited_members() 254 | %> 255 | % if members: 256 |

Inherited members

257 |
    258 | % for cls, mems in members: 259 |
  • ${link(cls)}: 260 |
      261 | % for m in mems: 262 |
    • ${link(m, name=m.name)}
    • 263 | % endfor 264 |
    265 | 266 |
  • 267 | % endfor 268 |
269 | % endif 270 | % endif 271 | 272 |
273 | % endfor 274 |
275 | % endif 276 |
277 | 278 | 279 | <%def name="module_index(module)"> 280 | <% 281 | variables = module.variables(sort=sort_identifiers) 282 | classes = module.classes(sort=sort_identifiers) 283 | functions = module.functions(sort=sort_identifiers) 284 | submodules = module.submodules() 285 | supermodule = module.supermodule 286 | %> 287 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | <% 372 | module_list = 'modules' in context.keys() # Whether we're showing module list in server mode 373 | %> 374 | 375 | % if module_list: 376 | Python module list 377 | 378 | % else: 379 | ${module.name} API documentation 380 | 381 | % endif 382 | 383 | 384 | 385 | % if syntax_highlighting: 386 | 387 | %endif 388 | 389 | <%namespace name="css" file="css.mako" /> 390 | 391 | 392 | 393 | 394 | % if google_analytics: 395 | 399 | % endif 400 | 401 | % if google_search_query: 402 | 403 | 404 | 408 | % endif 409 | 410 | % if latex_math: 411 | 412 | % endif 413 | 414 | % if syntax_highlighting: 415 | 416 | 417 | % endif 418 | 419 | <%include file="head.mako"/> 420 | 421 | 422 |
423 | % if module_list: 424 |
425 | ${show_module_list(modules)} 426 |
427 | % else: 428 |
429 | ${show_module(module)} 430 |
431 | ${module_index(module)} 432 | % endif 433 |
434 | 435 | 439 | 440 | % if http_server and module: ## Auto-reload on file change in dev mode 441 | 449 | % endif 450 | 451 | 452 | -------------------------------------------------------------------------------- /tests/test_seedir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Unit tests for seedir. 4 | 5 | Previously written for unittests, and updated for pytest. 6 | 7 | Test methods MUST start with "test" 8 | """ 9 | 10 | import os 11 | 12 | import pytest 13 | 14 | import seedir as sd 15 | from seedir.errors import FakedirError 16 | from seedir.folderstructure import FakeDirStructure as FDS 17 | 18 | # ---- Test seedir strings 19 | 20 | example = """mypkg/ 21 | __init__.py 22 | app.py 23 | view.py 24 | test/ 25 | __init__.py 26 | test_app.py 27 | test_view.py""" 28 | 29 | no_init = """mypkg/ 30 | app.py 31 | view.py 32 | test/ 33 | test_app.py 34 | test_view.py""" 35 | 36 | example_with_comments="""mypkg/ 37 | __init__.py #some comments 38 | app.py ##### more comments 39 | view.py #how about## this one 40 | test/ 41 | __init__.py 42 | test_app.py 43 | test_view.py""" 44 | 45 | # sd.randomdir(seed=456) 46 | large_example = """MyFakeDir/ 47 | ├─Vogel.txt 48 | ├─monkish.txt 49 | ├─jowly.txt 50 | ├─scrooge/ 51 | │ ├─light.txt 52 | │ ├─reliquary.txt 53 | │ ├─sandal/ 54 | │ ├─paycheck/ 55 | │ │ ├─electrophoresis.txt 56 | │ │ └─Pyongyang/ 57 | │ └─patrimonial/ 58 | ├─Uganda/ 59 | └─pedantic/ 60 | └─cataclysmic.txt""" 61 | 62 | large_example_access_denied = """MyFakeDir/ 63 | ├─Vogel.txt 64 | ├─monkish.txt 65 | ├─jowly.txt 66 | ├─scrooge/ [ACCESS DENIED] 67 | ├─Uganda/ [ACCESS DENIED] 68 | └─pedantic/ [ACCESS DENIED]""" 69 | 70 | large_example_bummer = """MyFakeDir/ 71 | ├─Vogel.txt 72 | ├─monkish.txt 73 | ├─jowly.txt 74 | ├─scrooge/<- BUMMER! 75 | ├─Uganda/<- BUMMER! 76 | └─pedantic/<- BUMMER!""" 77 | 78 | limit0_nobeyond = 'MyFakeDir/' 79 | 80 | limit0_beyond_content = """MyFakeDir/ 81 | └─3 folder(s), 3 file(s)""" 82 | 83 | depthlimit1 = '''MyFakeDir/ 84 | ├─Vogel.txt 85 | ├─monkish.txt 86 | ├─jowly.txt 87 | ├─scrooge/ 88 | ├─Uganda/ 89 | └─pedantic/''' 90 | 91 | depthlimit1_beyond_content = '''MyFakeDir/ 92 | ├─Vogel.txt 93 | ├─monkish.txt 94 | ├─jowly.txt 95 | ├─scrooge/ 96 | │ └─3 folder(s), 2 file(s) 97 | ├─Uganda/ 98 | │ └─0 folder(s), 0 file(s) 99 | └─pedantic/ 100 | └─0 folder(s), 1 file(s)''' 101 | 102 | depthlimit1_beyond_content_exclude = '''MyFakeDir/ 103 | ├─scrooge/ 104 | │ └─3 folder(s), 0 file(s) 105 | ├─Uganda/ 106 | │ └─0 folder(s), 0 file(s) 107 | └─pedantic/ 108 | └─0 folder(s), 0 file(s)''' 109 | 110 | complex_sort = '''MyFakeDir/ 111 | ├─monkish.txt 112 | ├─Vogel.txt 113 | ├─jowly.txt 114 | ├─pedantic/ 115 | │ └─cataclysmic.txt 116 | ├─scrooge/ 117 | │ ├─reliquary.txt 118 | │ ├─light.txt 119 | │ ├─patrimonial/ 120 | │ ├─paycheck/ 121 | │ │ ├─electrophoresis.txt 122 | │ │ └─Pyongyang/ 123 | │ └─sandal/ 124 | └─Uganda/''' 125 | 126 | complex_inclusion = '''MyFakeDir/ 127 | ├─Vogel.txt 128 | ├─monkish.txt 129 | ├─jowly.txt 130 | ├─scrooge/ 131 | │ ├─light.txt 132 | │ └─sandal/ 133 | └─pedantic/''' 134 | 135 | fmt_expand_single = '''MyFakeDir/ 136 | ├─Vogel.txt 137 | ├─monkish.txt 138 | ├─jowly.txt 139 | ├─scrooge/ 140 | │ ├─light.txt 141 | │ ├─reliquary.txt 142 | │ ├─sandal/ 143 | │ ├─paycheck/ 144 | │ │ ├─electrophoresis.txt 145 | │ │ └─Pyongyang/ 146 | │ └─patrimonial/ 147 | ├─Uganda/ 148 | └─pedantic/''' 149 | 150 | fmt_expand_single_partial = '''MyFakeDir/ 151 | ├─Vogel.txt 152 | ├─monkish.txt 153 | ├─jowly.txt 154 | ├─scrooge/ 155 | │ ├─light.txt 156 | │ ├─reliquary.txt 157 | │ ├─sandal/ 158 | │ ├─paycheck/ 159 | │ └─patrimonial/ 160 | ├─Uganda/ 161 | └─pedantic/''' 162 | 163 | fmt_notbeyond = """MyFakeDir/ 164 | ->Vogel.txt 165 | └─3 folder(s), 2 file(s)""" 166 | 167 | fmt_with_mask = '''MyFakeDir/ 168 | ├─scrooge/ 169 | │ ├─light.txt 170 | │ └─reliquary.txt 171 | ├─Uganda/ 172 | └─pedantic/ 173 | └─cataclysmic.txt''' 174 | 175 | # variables 176 | styles = ['lines', 'dash', 'spaces', 'arrow', 'plus', 'emoji'] 177 | try: 178 | import emoji 179 | except ImportError: 180 | styles.remove('emoji') 181 | 182 | # realdir for testing on 183 | testdir = os.path.dirname(os.path.abspath(__file__)) 184 | 185 | # custom FakeDirStructure for testing errors 186 | class ErrorRaisingFDS(FDS): 187 | 188 | def listdir(self, item): 189 | if self.isdir(item) and item.depth != 0: 190 | raise FakedirError('Oops!!!') 191 | else: 192 | return item.listdir() 193 | 194 | # ---- Test cases 195 | 196 | class PrintSomeDirs: 197 | 198 | print('\n--------------------' 199 | '\n\nTesting seedir.seedir() against {}:\n\n' 200 | '--------------------' 201 | '\n'.format(testdir)) 202 | def test_a_print_userprofile(self): 203 | print('Basic seedir (depthlimit=2, itemlimit=10):\n') 204 | sd.seedir(testdir, depthlimit=2, itemlimit=10) 205 | 206 | def test_b_styles(self): 207 | print('\nDifferent Styles (depthlimit=1, itemlimit=5):') 208 | for style in sd.STYLE_DICT.keys(): 209 | print('\n{}:\n'.format(style)) 210 | sd.seedir(testdir, style=style, depthlimit=1, itemlimit=5) 211 | 212 | def test_c_custom_styles(self): 213 | print('\nCustom Styles (depthlimit=1, itemlimit=5):') 214 | sd.seedir(testdir, depthlimit=1, itemlimit=5, space='>>', 215 | split='>>', extend='II', final='->', 216 | folderstart='Folder: ', filestart='File: ') 217 | 218 | def test_d_indent(self): 219 | print('\nDifferent Indents (depthlimit=1, itemlimit=5):') 220 | for i in list(range(3)) + [8]: 221 | print('\nindent={}:\n'.format(str(i))) 222 | sd.seedir(testdir, depthlimit=1, itemlimit=5, indent=i) 223 | 224 | def test_e_beyond(self): 225 | print('\nItems Beyond Limit (depthlimit=1, itemlimit=1, beyond="content")') 226 | sd.seedir(testdir, itemlimit=1, beyond='content') 227 | 228 | def test_improper_kwargs(self): 229 | with pytest.raises(ValueError): 230 | sd.seedir(testdir, spacing=False) 231 | 232 | class TestSeedirStringFormatting: 233 | def test_get_base_header_0(self): 234 | a = '| ' 235 | b = ' ' 236 | assert FDS().get_base_header([0], a, b) == '' 237 | 238 | def test_get_base_header_013(self): 239 | a = '| ' 240 | b = ' ' 241 | assert FDS().get_base_header([0, 1, 3], a, b) == '| | ' 242 | 243 | def test_get_base_header_empty(self): 244 | a = '| ' 245 | b = ' ' 246 | assert FDS().get_base_header([], a, b) == '' 247 | 248 | def test_STYLE_DICT_members(self): 249 | keys = set(sd.STYLE_DICT.keys()) 250 | assert keys == set(styles) 251 | 252 | @pytest.mark.parametrize('style', styles) 253 | def test_get_style_args_all_accessible(self, style): 254 | d = sd.get_styleargs(style) 255 | assert isinstance(d, dict) 256 | 257 | def test_access_missing_style(self): 258 | with pytest.raises(ValueError): 259 | _ = sd.get_styleargs('missing_style') 260 | 261 | def test_get_style_args_deepcopy(self): 262 | x = sd.STYLE_DICT['lines'] 263 | y = sd.get_styleargs('lines') 264 | assert x is not y 265 | 266 | def test_format_indent_4(self): 267 | a = sd.get_styleargs('lines') 268 | sd.printing.format_indent(a, indent=4) 269 | chars = ['extend', 'space', 'split', 'final'] 270 | assert all(len(a[c])==4 for c in chars) 271 | 272 | def test_format_indent_1(self): 273 | a = sd.get_styleargs('lines') 274 | sd.printing.format_indent(a, indent=1) 275 | chars = ['extend', 'space', 'split', 'final'] 276 | assert all(len(a[c])==1 for c in chars) 277 | 278 | def test_words_list_start(self): 279 | assert sd.printing.words[0] == 'a' 280 | 281 | def test_words_list_length(self): 282 | assert len(sd.printing.words) == 25487 283 | 284 | class TestFakeDirReading: 285 | def test_read_string(self): 286 | x = sd.fakedir_fromstring(example) 287 | assert isinstance(x, sd.FakeDir) 288 | 289 | def test_parse_comments_on(self): 290 | x = sd.fakedir_fromstring(example) 291 | y = sd.fakedir_fromstring(example_with_comments) 292 | assert x.get_child_names() == y.get_child_names() 293 | 294 | def test_parse_comments_off(self): 295 | x = sd.fakedir_fromstring(example) 296 | y = sd.fakedir_fromstring(example_with_comments, parse_comments=False) 297 | assert x.get_child_names() != y.get_child_names() 298 | 299 | class TestFakeDir: 300 | def test_count_fake_folders(self): 301 | x = sd.fakedir_fromstring(example) 302 | assert FDS().count_folders(x.listdir()) == 1 303 | 304 | def test_count_fake_files(self): 305 | x = sd.fakedir_fromstring(example) 306 | assert FDS().count_files(x.listdir()) == 3 307 | 308 | def test_sort_fakedir(self): 309 | x = sd.fakedir_fromstring(example).listdir() 310 | sort = FDS().sort_dir(x, sort_reverse=True, sort_key=lambda x : x[1]) 311 | sort = [f.name for f in sort] 312 | correct = ['app.py', 'view.py', 'test', '__init__.py'] 313 | assert sort == correct 314 | 315 | def test_exclude_files_and_reread(self): 316 | x = sd.fakedir_fromstring(example) 317 | y = x.seedir(printout=False, exclude_files=r'.*\..*', regex=True) 318 | z = sd.fakedir_fromstring(y) 319 | assert set(z.get_child_names()) == set(['test']) 320 | 321 | def test_include_files_and_reread(self): 322 | x = sd.fakedir_fromstring(example) 323 | y = x.seedir(printout=False, include_files=['app.py', 'view.py'], 324 | regex=False) 325 | z = sd.fakedir_fromstring(y) 326 | assert set(z.get_child_names()) == set(['app.py', 'view.py', 'test', ]) 327 | 328 | def test_delete_string_names(self): 329 | x = sd.randomdir() 330 | x.delete(x.get_child_names()) 331 | assert len(x.listdir()) == 0 332 | 333 | def test_delete_objects(self): 334 | x = sd.randomdir() 335 | x.delete(x.listdir()) 336 | assert len(x.listdir()) == 0 337 | 338 | def test_set_parent(self): 339 | x = sd.fakedir_fromstring(example) 340 | x['test/test_app.py'].parent = x 341 | assert 'test_app.py' in x.get_child_names() 342 | 343 | def test_walk_apply(self): 344 | def add_0(f): 345 | f.name += ' 0' 346 | x = sd.fakedir_fromstring(example) 347 | x.walk_apply(add_0) 348 | last_chars = [f[-1] for f in x.get_child_names()] 349 | assert set(last_chars) == set('0') 350 | 351 | def test_depth_setting(self): 352 | x = sd.fakedir_fromstring(example) 353 | x['test'].create_folder('A') 354 | x['test/A'].create_folder('B') 355 | x['test/A/B'].create_file('boris.txt') 356 | obj = x['test/A/B/boris.txt'] 357 | depth_a = obj.depth 358 | obj.parent = x 359 | depth_b = obj.depth 360 | assert (depth_a, depth_b) == (4, 1) 361 | 362 | def test_randomdir_seed(self): 363 | x = sd.randomdir(seed=4.21) 364 | y = sd.randomdir(seed=4.21) 365 | assert x.get_child_names() == y.get_child_names() 366 | 367 | def test_empty_fakedir_has_no_children(self): 368 | x = sd.FakeDir('BORIS') 369 | assert len(x.get_child_names()) == 0 370 | 371 | def test_populate_adds_children(self): 372 | x = sd.FakeDir('BORIS') 373 | sd.populate(x) 374 | assert len(x.get_child_names()) > 0 375 | 376 | def test_copy_equal(self): 377 | x = sd.randomdir(seed=7) 378 | y = x.copy() 379 | assert x.seedir(printout=False) == y.seedir(printout=False) 380 | 381 | def test_copy_unlinked(self): 382 | def pallindrome(f): 383 | f.name = f.name + f.name[::-1] 384 | x = sd.fakedir_fromstring(large_example) 385 | 386 | before = x.seedir(printout=False) 387 | 388 | y = x.copy() 389 | y.walk_apply(pallindrome) 390 | 391 | after = x.seedir(printout=False) 392 | assert before == after 393 | 394 | class TestMask: 395 | def test_mask_no_folders_or_files(self): 396 | def foo(x): 397 | if os.path.isdir(x) or os.path.isfile(x): 398 | return False 399 | 400 | s = sd.seedir(testdir, printout=False, depthlimit=2, itemlimit=10, mask=foo,) 401 | s = s.split('\n') 402 | assert len(s) == 1 403 | 404 | def test_mask_always_false(self): 405 | def bar(x): 406 | return False 407 | s = sd.seedir(testdir, printout=False, depthlimit=2, itemlimit=10, mask=bar) 408 | s = s.split('\n') 409 | assert len(s) == 1 410 | 411 | def test_mask_fakedir_fromstring(self): 412 | x = sd.fakedir_fromstring(example) 413 | s = x.seedir(printout=False, mask=lambda x : not x.name[0] == '_', 414 | style='spaces', indent=4) 415 | assert no_init == s 416 | 417 | def test_mask_fakedir(self): 418 | def foo(x): 419 | if os.path.isdir(x) or os.path.isfile(x): 420 | return False 421 | f = sd.fakedir(testdir, mask=foo) 422 | assert len(f.listdir()) == 0 423 | 424 | class TestFolderStructure: 425 | 426 | def test_many_randomdirs(self): 427 | seeds = range(1000) 428 | results = [] 429 | for i in seeds: 430 | r = sd.randomdir(seed=i) 431 | s = r.seedir(printout=False) 432 | f = sd.fakedir_fromstring(s) 433 | results.append(s == f.seedir(printout=False)) 434 | assert all(results) 435 | 436 | def test_itemlimit0_nobeyond(self): 437 | ans = limit0_nobeyond 438 | f = sd.fakedir_fromstring(large_example) 439 | s = f.seedir(printout=False, itemlimit=0) 440 | assert ans == s 441 | 442 | def test_depthlimit0_nobeyond(self): 443 | ans = limit0_nobeyond 444 | f = sd.fakedir_fromstring(large_example) 445 | s = f.seedir(printout=False, depthlimit=0) 446 | assert ans == s 447 | 448 | def test_itemlimit0_beyond_content(self): 449 | ans = limit0_beyond_content 450 | f = sd.fakedir_fromstring(large_example) 451 | s = f.seedir(printout=False, itemlimit=0, beyond='content') 452 | assert ans == s 453 | 454 | def test_depthlimit0_beyond_content(self): 455 | ans = limit0_beyond_content 456 | f = sd.fakedir_fromstring(large_example) 457 | s = f.seedir(printout=False, depthlimit=0, beyond='content') 458 | assert ans == s 459 | 460 | def test_depthlimit1(self): 461 | ans = depthlimit1 462 | f = sd.fakedir_fromstring(large_example) 463 | s = f.seedir(printout=False, depthlimit=1) 464 | assert ans == s 465 | 466 | def test_depthlimit1_beyond_content(self): 467 | ans = depthlimit1_beyond_content 468 | f = sd.fakedir_fromstring(large_example) 469 | s = f.seedir(printout=False, depthlimit=1, beyond='content') 470 | assert ans == s 471 | 472 | def test_depthlimit1_beyond_content_exclude(self): 473 | ans = depthlimit1_beyond_content_exclude 474 | f = sd.fakedir_fromstring(large_example) 475 | s = f.seedir(printout=False, 476 | depthlimit=1, 477 | beyond='content', 478 | exclude_files=r'.*\.txt', 479 | regex=True) 480 | assert ans == s 481 | 482 | def test_complex_sort(self): 483 | ans = complex_sort 484 | params = dict(sort=True, sort_reverse=True, 485 | sort_key = lambda x: len(x), first='files') 486 | f = sd.fakedir_fromstring(large_example) 487 | s = f.seedir(printout=False,**params) 488 | assert ans == s 489 | 490 | def test_complex_inclusion(self): 491 | ans = complex_inclusion 492 | params = dict(include_folders=['sandal', 'scrooge', 'pedantic'], 493 | exclude_folders='sandal', 494 | exclude_files='^Vogel', 495 | include_files='^.[oi]', 496 | regex=True) 497 | f = sd.fakedir_fromstring(large_example) 498 | s = f.seedir(printout=False,**params) 499 | assert ans == s 500 | 501 | class TestFormatter: 502 | 503 | def test_formatter_beyond(self): 504 | 505 | def fmt(p): 506 | 507 | d = {'split':'->', 'final':'->'} 508 | 509 | return d 510 | 511 | ans = fmt_notbeyond 512 | f = sd.fakedir_fromstring(large_example) 513 | s = f.seedir(formatter=fmt, itemlimit=1, beyond='content', printout=False) 514 | assert s == ans 515 | 516 | 517 | def test_formatter_no_return(self): 518 | 519 | f = sd.fakedir_fromstring(large_example) 520 | s = f.seedir(formatter=lambda x: None, printout=False) 521 | assert s == large_example 522 | 523 | def test_expand_one_folder_sticky(self): 524 | 525 | def fmt(p): 526 | 527 | d = {} 528 | if p.name == 'scrooge': 529 | d['depthlimit'] = None 530 | 531 | return d 532 | 533 | ans = fmt_expand_single 534 | f = sd.fakedir_fromstring(large_example) 535 | s = f.seedir(formatter=fmt, depthlimit=1, sticky_formatter=True, printout=False) 536 | assert ans == s 537 | 538 | def test_expand_one_folder_nosticky(self): 539 | 540 | def fmt(p): 541 | 542 | d = {} 543 | if p.name == 'scrooge': 544 | d['depthlimit'] = None 545 | 546 | return d 547 | 548 | ans = fmt_expand_single_partial 549 | f = sd.fakedir_fromstring(large_example) 550 | s = f.seedir(formatter=fmt, depthlimit=1, printout=False) 551 | assert ans == s 552 | 553 | def test_mask_with_fmt(self): 554 | 555 | def fmt(p): 556 | 557 | d = {} 558 | 559 | def case1(p): 560 | return not p.name.endswith('.txt') 561 | 562 | def case2(p): 563 | return p.name.endswith('.txt') 564 | 565 | if p.depth == 0: 566 | d['mask'] = case1 567 | 568 | else: 569 | d['mask'] = case2 570 | 571 | return d 572 | 573 | ans = fmt_with_mask 574 | f = sd.fakedir_fromstring(large_example) 575 | s = f.seedir(formatter=fmt, printout=False) 576 | assert s == ans 577 | 578 | class TestTupleItemLimit: 579 | 580 | def count_folder_children(self, f): 581 | output = [] 582 | foo = lambda x: output.append(len([a for a in f.listdir() if a.isdir()])) 583 | f.walk_apply(foo) 584 | return output 585 | 586 | def count_file_children(self, f): 587 | output = [] 588 | foo = lambda x: output.append(len([a for a in f.listdir() if not a.isdir()])) 589 | f.walk_apply(foo) 590 | return output 591 | 592 | def make_letter_example(self): 593 | f = sd.FakeDir('example') 594 | f.create_folder(['a', 'b', 'c', 'd']) 595 | f.create_file(['e', 'f', 'g', 'h']) 596 | return f 597 | 598 | def test_None_None(self): 599 | f = sd.fakedir_fromstring(large_example) 600 | normal = f.seedir(printout=False) 601 | test = f.seedir(itemlimit=(None, None), printout=False) 602 | assert normal == test 603 | 604 | def test_None_1(self): 605 | start = sd.fakedir_fromstring(large_example) 606 | s = start.seedir(itemlimit=(None, 1), first='files', printout=False) 607 | end = sd.fakedir_fromstring(s) 608 | ans = self.count_file_children(end) 609 | assert all([x <= 1 for x in ans]) 610 | 611 | def test_1_None(self): 612 | start = sd.fakedir_fromstring(large_example) 613 | s = start.seedir(itemlimit=(1, None), first='folders', printout=False) 614 | end = sd.fakedir_fromstring(s) 615 | ans = self.count_folder_children(end) 616 | assert all([x <= 1 for x in ans]) 617 | 618 | def test_1_1(self): 619 | start = sd.fakedir_fromstring(large_example) 620 | s = start.seedir(itemlimit=(1, None), first='folders', printout=False) 621 | end = sd.fakedir_fromstring(s) 622 | ans = self.count_folder_children(end) 623 | assert all([x <= 1 for x in ans]) 624 | 625 | def test_filter_letter_example_2_2(self): 626 | e = self.make_letter_example() 627 | s = e.seedir(itemlimit=(2, 2), sort=True, printout=False) 628 | f = sd.fakedir_fromstring(s) 629 | assert set(f.get_child_names()) == set(['a', 'b', 'e', 'f']) 630 | 631 | def test_filter_letter_example_2_None(self): 632 | e = self.make_letter_example() 633 | s = e.seedir(itemlimit=(2, None), sort=True, printout=False) 634 | f = sd.fakedir_fromstring(s) 635 | assert set(f.get_child_names()) == set(['a', 'b', 'e', 'f', 'g', 'h']) 636 | 637 | def test_filter_letter_example_None_0(self): 638 | e = self.make_letter_example() 639 | s = e.seedir(itemlimit=(None, 0), sort=True, printout=False) 640 | f = sd.fakedir_fromstring(s) 641 | assert set(f.get_child_names()) == set(['a', 'b', 'c', 'd']) 642 | 643 | def test_filter_letter_example_0_0(self): 644 | e = self.make_letter_example() 645 | s = e.seedir(itemlimit=(0, 0), sort=True, printout=False) 646 | f = sd.fakedir_fromstring(s) 647 | assert len(f.get_child_names()) == 0 648 | 649 | def test_works_with_list(self): 650 | f = sd.fakedir_fromstring(large_example) 651 | s = f.seedir(itemlimit=[0, None], printout=False) 652 | split = s.split('\n') 653 | endswithtxt = [x.endswith('txt') for x in split[1:]] 654 | assert all(endswithtxt) 655 | 656 | class TestErrorHandlingArgs: 657 | 658 | def test_handle_errors_correct_type(self): 659 | x = ErrorRaisingFDS() 660 | f = sd.fakedir_fromstring(large_example) 661 | s = x(f, printout=False, 662 | acceptable_listdir_errors=FakedirError, 663 | denied_string=' [ACCESS DENIED]') 664 | assert s == large_example_access_denied 665 | 666 | def test_handle_errors_incorrect_type(self): 667 | x = ErrorRaisingFDS() 668 | f = sd.fakedir_fromstring(large_example) 669 | with pytest.raises(sd.errors.FakedirError): 670 | _ = x(f, printout=False, 671 | acceptable_listdir_errors=PermissionError, 672 | denied_string=' [ACCESS DENIED]') 673 | 674 | def test_handle_errors_None(self): 675 | x = ErrorRaisingFDS() 676 | f = sd.fakedir_fromstring(large_example) 677 | with pytest.raises(sd.errors.FakedirError): 678 | _ = x(f, printout=False, 679 | acceptable_listdir_errors=None, 680 | denied_string=' [ACCESS DENIED]') 681 | 682 | def test_handle_errors_tuple(self): 683 | x = ErrorRaisingFDS() 684 | f = sd.fakedir_fromstring(large_example) 685 | s = x(f, printout=False, 686 | acceptable_listdir_errors=(FakedirError, PermissionError), 687 | denied_string=' [ACCESS DENIED]') 688 | assert s == large_example_access_denied 689 | 690 | def test_handle_errors_different_string(self): 691 | x = ErrorRaisingFDS() 692 | f = sd.fakedir_fromstring(large_example) 693 | s = x(f, printout=False, 694 | acceptable_listdir_errors=FakedirError, 695 | denied_string='<- BUMMER!') 696 | assert s == large_example_bummer 697 | 698 | -------------------------------------------------------------------------------- /docs/gettingstarted.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | The following examples will cover what you can do with seedir. We can start by importing the package with the `sd` alias: 4 | 5 | ```python 6 | >>> import seedir as sd 7 | 8 | ``` 9 | 10 | ## Displaying folder trees 11 | 12 | The primary function of seedir is to create plain text diagrams of folders, for use in blogs, examples, Q&As, etc. The GitHub repo for seedir includes an example folder (`seedir/seedir/exampledir`) which will be used here. 13 | 14 | To printout out the structure of this folder, you can use the primary `seedir.realdir.seedir()` function: 15 | 16 | ```python 17 | >>> path = 'exampledir' 18 | >>> sd.seedir(path) 19 | exampledir/ 20 | ├─jowly.pdf 21 | ├─monkish.txt 22 | ├─pedantic/ 23 | │ └─cataclysmic.txt 24 | ├─scrooge/ 25 | │ ├─light.pdf 26 | │ ├─paycheck/ 27 | │ │ └─electrophoresis.txt 28 | │ └─reliquary.pdf 29 | └─Vogel.txt 30 | 31 | ``` 32 | 33 | By default, **the output is printed**. To return a string instead, use the `printout` argument: 34 | 35 | ```python 36 | >>> s = sd.seedir(path, printout=False) 37 | >>> print(type(s)) 38 | 39 | 40 | ``` 41 | 42 | ## Trimming folder trees 43 | 44 | Sometimes a directory is too large, is private, or contains certain irrelevant files. To handle this, there are a few arguments which allow you to edit the items included in the output. 45 | 46 | ### Including & excluding folders & files 47 | 48 | One way is to call out specific folders of files to include or exclude: 49 | 50 | ```python 51 | >>> sd.seedir(path, include_folders=['scrooge','paycheck'], exclude_files='reliquary.pdf') 52 | exampledir/ 53 | ├─jowly.pdf 54 | ├─monkish.txt 55 | ├─scrooge/ 56 | │ ├─light.pdf 57 | │ └─paycheck/ 58 | │ └─electrophoresis.txt 59 | └─Vogel.txt 60 | 61 | ``` 62 | 63 | By passing `regex=True`, these "include" and "exclude" arguments also support regular expressions, : 64 | 65 | ```python 66 | >>> sd.seedir(path, include_files='.*\.pdf$', regex=True) # all PDFs 67 | exampledir/ 68 | ├─jowly.pdf 69 | ├─pedantic/ 70 | └─scrooge/ 71 | ├─light.pdf 72 | ├─paycheck/ 73 | └─reliquary.pdf 74 | 75 | ``` 76 | 77 | ### Masking 78 | 79 | You can also use the `mask` parameter to functionally filter out items in the folder. Pathnames are passed to the mask function, and items are kept if `True` is returned: 80 | 81 | ```python 82 | >>> import os 83 | 84 | >>> def foo(x): # omits folders with more than 2 items 85 | ... if os.path.isdir(x) and len(os.listdir(x)) > 2: 86 | ... return False 87 | ... return True 88 | 89 | >>> sd.seedir(path, mask=foo) 90 | exampledir/ 91 | ├─jowly.pdf 92 | ├─monkish.txt 93 | ├─pedantic/ 94 | │ └─cataclysmic.txt 95 | └─Vogel.txt 96 | 97 | ``` 98 | 99 | ### Limiting the depth or number of items 100 | 101 | You can also wholly limit the output by providing the `depthlimit` or `itemlimit` arguments. Respectively, these arguments limit the depth of folders to enter and the number of items to include per folder. 102 | 103 | ```python 104 | >>> sd.seedir(path, depthlimit=1) 105 | exampledir/ 106 | ├─jowly.pdf 107 | ├─monkish.txt 108 | ├─pedantic/ 109 | ├─scrooge/ 110 | └─Vogel.txt 111 | 112 | >>> sd.seedir(path, itemlimit=3) 113 | exampledir/ 114 | ├─jowly.pdf 115 | ├─monkish.txt 116 | └─pedantic/ 117 | └─cataclysmic.txt 118 | 119 | ``` 120 | 121 | **New in v0.5.0!** You can also pass a 2-tuple to `itemlimit` to have separate limits for folders and files, respectively: 122 | 123 | ```python 124 | >>> sd.seedir(path, itemlimit=(None, 1)) 125 | exampledir/ 126 | ├─scrooge/ 127 | │ ├─light.pdf 128 | │ └─paycheck/ 129 | │ └─electrophoresis.txt 130 | ├─jowly.pdf 131 | └─pedantic/ 132 | └─cataclysmic.txt 133 | 134 | ``` 135 | 136 | 137 | 138 | As `seedir.realdir.seedir()` uses recursion, these arguments can hedge the traversal of deep, complicated folders. 139 | 140 | When limiting the tree, using the `beyond` argument can be helpful to show what is being cut. The special value `'content'` shows the number of folders and files: 141 | 142 | ```python 143 | >>> sd.seedir(path, depthlimit=1, beyond='content') 144 | exampledir/ 145 | ├─jowly.pdf 146 | ├─monkish.txt 147 | ├─pedantic/ 148 | │ └─0 folder(s), 1 file(s) 149 | ├─scrooge/ 150 | │ └─1 folder(s), 2 file(s) 151 | └─Vogel.txt 152 | 153 | ``` 154 | 155 | ### Sorting 156 | 157 | Especially when using the `itemlimit`, but also generally, you may want to sort the output to determine which items appear first. You can apply a general sort using `sort=True`: 158 | 159 | ```python 160 | >>> sd.seedir(path, itemlimit=4, sort=True) 161 | exampledir/ 162 | ├─Vogel.txt 163 | ├─jowly.pdf 164 | ├─monkish.txt 165 | └─pedantic/ 166 | └─cataclysmic.txt 167 | 168 | ``` 169 | 170 | There are additional reverse and key arguments (akin to `sorted()` or `list.sort()`) which allow you to customize the sorting: 171 | 172 | ```python 173 | >>> sd.seedir(path, sort=True, sort_reverse=True, sort_key=lambda s : len(s)) 174 | exampledir/ 175 | ├─monkish.txt 176 | ├─jowly.pdf 177 | ├─Vogel.txt 178 | ├─pedantic/ 179 | │ └─cataclysmic.txt 180 | └─scrooge/ 181 | ├─reliquary.pdf 182 | ├─light.pdf 183 | └─paycheck/ 184 | └─electrophoresis.txt 185 | 186 | ``` 187 | 188 | The `first` argument allows you to select whether files or folders appear first: 189 | 190 | ```python 191 | >>> sd.seedir(path, first='files') 192 | exampledir/ 193 | ├─Vogel.txt 194 | ├─jowly.pdf 195 | ├─monkish.txt 196 | ├─pedantic/ 197 | │ └─cataclysmic.txt 198 | └─scrooge/ 199 | ├─light.pdf 200 | ├─reliquary.pdf 201 | └─paycheck/ 202 | └─electrophoresis.txt 203 | 204 | ``` 205 | 206 | ## Styles 💅 207 | 208 | `seedir.realdir.seedir()` has a few builtin styles for formatting the output of the folder tree: 209 | 210 | ```python 211 | >>> sd.seedir(path, style='emoji') 212 | 📁 exampledir/ 213 | ├─📄 jowly.pdf 214 | ├─📄 monkish.txt 215 | ├─📁 pedantic/ 216 | │ └─📄 cataclysmic.txt 217 | ├─📁 scrooge/ 218 | │ ├─📄 light.pdf 219 | │ ├─📁 paycheck/ 220 | │ │ └─📄 electrophoresis.txt 221 | │ └─📄 reliquary.pdf 222 | └─📄 Vogel.txt 223 | 224 | ``` 225 | 226 | To make use of the 'emoji' style, install [emoji](https://pypi.org/project/emoji/) (`pip install emoji`) or use `pip install seedir[emoji]`. 227 | 228 | For any builtin style, you can customize the indent size: 229 | 230 | ```python 231 | >>> sd.seedir(path, style='spaces', indent=4) 232 | exampledir/ 233 | jowly.pdf 234 | monkish.txt 235 | pedantic/ 236 | cataclysmic.txt 237 | scrooge/ 238 | light.pdf 239 | paycheck/ 240 | electrophoresis.txt 241 | reliquary.pdf 242 | Vogel.txt 243 | 244 | >>> sd.seedir(path, style='plus', indent=6) 245 | exampledir/ 246 | +-----jowly.pdf 247 | +-----monkish.txt 248 | +-----pedantic/ 249 | | +-----cataclysmic.txt 250 | +-----scrooge/ 251 | | +-----light.pdf 252 | | +-----paycheck/ 253 | | | +-----electrophoresis.txt 254 | | +-----reliquary.pdf 255 | +-----Vogel.txt 256 | 257 | ``` 258 | 259 | Each style is basically a collection of string "tokens" which are combined to form the header of each printed line (based on the depth and folder structure). You can see these tokens using `seedir.printing.get_styleargs()`: 260 | 261 | ```python 262 | >>> sd.get_styleargs('emoji') 263 | {'split': '├─', 'extend': '│ ', 'space': ' ', 'final': '└─', 'folderstart': '📁 ', 'filestart': '📄 ', 'folderend': '/', 'fileend': ''} 264 | 265 | ``` 266 | 267 | You can pass any of these tokens as `**kwargs` to explicitly customize styles with new symbols (note that passed tokens will not be affected by the `indent` parameter; it assumes you know how long you want them to be): 268 | 269 | ```python 270 | >>> sd.seedir(path, space=' ', extend='||', split='-}', final='\\\\', folderstart=' ', filestart=' ', folderend='%') 271 | exampledir% 272 | -} jowly.pdf 273 | -} monkish.txt 274 | -} pedantic% 275 | ||\\ cataclysmic.txt 276 | -} scrooge% 277 | ||-} light.pdf 278 | ||-} paycheck% 279 | ||||\\ electrophoresis.txt 280 | ||\\ reliquary.pdf 281 | \\ Vogel.txt 282 | 283 | ``` 284 | 285 | There are also `uniform` and `anystart` arguments for customizing multiple tokens at once: 286 | 287 | ```python 288 | >>> sd.seedir(path, uniform='----', anystart='>') 289 | >exampledir/ 290 | ---->jowly.pdf 291 | ---->monkish.txt 292 | ---->pedantic/ 293 | -------->cataclysmic.txt 294 | ---->scrooge/ 295 | -------->light.pdf 296 | -------->paycheck/ 297 | ------------>electrophoresis.txt 298 | -------->reliquary.pdf 299 | ---->Vogel.txt 300 | 301 | ``` 302 | 303 | ## Programmatic formatting 304 | 305 | [Following a user-raised issue](https://github.com/earnestt1234/seedir/issues/4), seedir has added a `formatter` parameter for enabling some programmatic editing of the folder tree. This can be useful when you want to alter the style of the diagram based on things like the depth, item name, file extension, etc. The path of each item (relative to the root) is passed to `formatter`, which returns new settings to use for that item. 306 | 307 | The following example edits the style tokens for items with particular names or file extensions: 308 | 309 | ```python 310 | >>> import os 311 | 312 | >>> def my_style(item): 313 | ... 314 | ... outdict = {} 315 | ... 316 | ... # get the extension 317 | ... ext = os.path.splitext(item)[1] 318 | ... 319 | ... if ext == '.txt': 320 | ... outdict['filestart'] = '✏️' 321 | ... 322 | ... if os.path.basename(item) == 'scrooge': 323 | ... outdict['folderstart'] = '👉' 324 | ... 325 | ... return outdict 326 | 327 | >>> sd.seedir(path, formatter=my_style) 328 | exampledir/ 329 | ├─jowly.pdf 330 | ├─✏️monkish.txt 331 | ├─pedantic/ 332 | │ └─✏️cataclysmic.txt 333 | ├─👉scrooge/ 334 | │ ├─light.pdf 335 | │ ├─paycheck/ 336 | │ │ └─✏️electrophoresis.txt 337 | │ └─reliquary.pdf 338 | └─✏️Vogel.txt 339 | 340 | ``` 341 | 342 | The `formatter` parameter can also dynamically set other options, such as the filtering arguments outlined above. The following example sets a mask that removes files, but only in the root directory: 343 | 344 | ```python 345 | >>> import os 346 | >>> def no_root_files(item): 347 | ... if os.path.basename(item) == 'exampledir': 348 | ... mask = lambda x: os.path.isdir(x) 349 | ... return {'mask': mask} 350 | 351 | >>> sd.seedir(path, formatter=no_root_files) 352 | exampledir/ 353 | ├─pedantic/ 354 | │ └─cataclysmic.txt 355 | └─scrooge/ 356 | ├─light.pdf 357 | ├─paycheck/ 358 | │ └─electrophoresis.txt 359 | └─reliquary.pdf 360 | 361 | ``` 362 | 363 | Note that by default, any changes set by the `formatter` are unset for sub-directories. You can turn on the `sticky_formatter` option to make changes persist: 364 | 365 | ``` 366 | >>> def mark(item): 367 | ... d = {} 368 | ... parent = os.path.basename(os.path.dirname(item)) 369 | ... if parent == 'pedantic': 370 | ... d['folderstart'] = '✔️ ' 371 | ... d['filestart'] = '✔️ ' 372 | ... if parent == 'scrooge': 373 | ... d['folderstart'] = '❌ ' 374 | ... d['filestart'] = '❌ ' 375 | ... 376 | ... return d 377 | 378 | # The function only makes changes to items in the 'pedantic' and 'scrooge' 379 | # folders explicilty. 380 | >>> sd.seedir(path, formatter=mark) 381 | exampledir/ 382 | ├─jowly.pdf 383 | ├─monkish.txt 384 | ├─pedantic/ 385 | │ └─✔️ cataclysmic.txt 386 | ├─scrooge/ 387 | │ ├─❌ light.pdf 388 | │ ├─❌ paycheck/ 389 | │ │ └─electrophoresis.txt 390 | │ └─❌ reliquary.pdf 391 | └─Vogel.txt 392 | 393 | # By passing `sticky_formatter`, changes are also applied to subdirectories 394 | >>> sd.seedir(path, formatter=mark, sticky_formatter=True) 395 | exampledir/ 396 | ├─jowly.pdf 397 | ├─monkish.txt 398 | ├─pedantic/ 399 | │ └─✔️ cataclysmic.txt 400 | ├─scrooge/ 401 | │ ├─❌ light.pdf 402 | │ ├─❌ paycheck/ 403 | │ │ └─❌ electrophoresis.txt 404 | │ └─❌ reliquary.pdf 405 | └─Vogel.txt 406 | 407 | ``` 408 | 409 | 410 | 411 | Some properties (e.g., the depth of the folder relative to the parent) are a little more difficult to determine with a system paths. However, switching to object-oriented FakeDirs (introduced in the following section) make accessing interfolder relations a little easier: 412 | 413 | ```python 414 | >>> # now items passed are FakeDir / FakeFile, not system path 415 | >>> def formatter(item): 416 | ... if item.depth > 1: # we can access the depth attribute 417 | ... return sd.get_styleargs('plus') 418 | 419 | >>> f = sd.fakedir(path) 420 | >>> f.seedir(formatter=formatter) 421 | exampledir/ 422 | ├─jowly.pdf 423 | ├─monkish.txt 424 | ├─pedantic/ 425 | | +-cataclysmic.txt 426 | ├─scrooge/ 427 | | +-light.pdf 428 | | +-paycheck/ 429 | | | +-electrophoresis.txt 430 | | +-reliquary.pdf 431 | └─Vogel.txt 432 | 433 | ``` 434 | 435 | ## Pathlib 436 | 437 | As of version 0.4.0, `sd.realdir.seedir()` also accepts [pathlib](https://docs.python.org/3/library/pathlib.html) objects: 438 | 439 | ```python 440 | >>> import pathlib 441 | >>> p = pathlib.Path(path) 442 | >>> p 443 | PosixPath('exampledir') 444 | 445 | >>> sd.seedir(p) 446 | exampledir/ 447 | ├─jowly.pdf 448 | ├─monkish.txt 449 | ├─pedantic/ 450 | │ └─cataclysmic.txt 451 | ├─scrooge/ 452 | │ ├─light.pdf 453 | │ ├─paycheck/ 454 | │ │ └─electrophoresis.txt 455 | │ └─reliquary.pdf 456 | └─Vogel.txt 457 | 458 | ``` 459 | 460 | Most of the features demonstrated above apply as usual. The only major difference is that arguments accepting callables (`mask` & `fromatter`) will now be passed `pathlib.Path` objects, instead of strings: 461 | 462 | ```python 463 | >>> mask = lambda x: x.is_dir() or x.suffix == '.txt' 464 | >>> sd.seedir(p, mask=mask) 465 | exampledir/ 466 | ├─scrooge/ 467 | │ └─paycheck/ 468 | │ └─electrophoresis.txt 469 | ├─monkish.txt 470 | ├─pedantic/ 471 | │ └─cataclysmic.txt 472 | └─Vogel.txt 473 | 474 | ``` 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | ## Fake directories 485 | 486 | You can also create or edit directory examples by using "fake directories" in `seedir`. These are Python representations of folders and files, which can be manipulated in ways similar to real directories. 487 | 488 | ### Making from scratch 489 | 490 | `seedir.fakedir.FakeDir` is a folder class; you can be used to initialize an example folder tree. `FakeDir` objects have a `seedir.fakedir.FakeDir.seedir()` method, which is fundamentally similar to `seedir.realdir.seedir()` for real system paths: 491 | 492 | ```python 493 | >>> x = sd.FakeDir('myfakedir') 494 | >>> x.seedir() 495 | myfakedir/ 496 | 497 | ``` 498 | 499 | To add items to `x`, you can create subitems, initialize new directories, or move existing ones: 500 | 501 | ```python 502 | >>> x.create_file(['__init__.py', 'main.py', 'styles.txt']) 503 | [FakeFile(myfakedir/__init__.py), FakeFile(myfakedir/main.py), FakeFile(myfakedir/styles.txt)] 504 | 505 | >>> y = sd.FakeDir('resources', parent=x) 506 | 507 | >>> z = sd.FakeDir('images') 508 | >>> z.parent = y 509 | 510 | >>> for n in ['a', 'b', 'c']: 511 | ... z.create_file(n + '.png') 512 | FakeFile(myfakedir/resources/images/a.png) 513 | FakeFile(myfakedir/resources/images/b.png) 514 | FakeFile(myfakedir/resources/images/c.png) 515 | 516 | >>> x.seedir(sort=True, first='folders') 517 | myfakedir/ 518 | ├─resources/ 519 | │ └─images/ 520 | │ ├─a.png 521 | │ ├─b.png 522 | │ └─c.png 523 | ├─__init__.py 524 | ├─main.py 525 | └─styles.txt 526 | 527 | ``` 528 | 529 | You can use path-like strings to index fake directories: 530 | 531 | ```python 532 | >>> x['resources/images/a.png'] 533 | FakeFile(myfakedir/resources/images/a.png) 534 | 535 | ``` 536 | 537 | You can also use the `listdir` or `get_child_names` methods to get the children of a folder (or their names): 538 | 539 | ```python 540 | >>> x['resources/images'].listdir() 541 | [FakeFile(myfakedir/resources/images/a.png), FakeFile(myfakedir/resources/images/b.png), FakeFile(myfakedir/resources/images/c.png)] 542 | 543 | >>> x['resources/images'].get_child_names() 544 | ['a.png', 'b.png', 'c.png'] 545 | 546 | ``` 547 | 548 | You can use the `delete` method to remove items by name or by object: 549 | 550 | ```python 551 | >>> x['resources/images'].delete([x['resources/images/a.png'], 'b.png']) 552 | >>> x.seedir() 553 | myfakedir/ 554 | ├─__init__.py 555 | ├─main.py 556 | ├─styles.txt 557 | └─resources/ 558 | └─images/ 559 | └─c.png 560 | 561 | ``` 562 | 563 | You can also move items within the tree: 564 | 565 | ```python 566 | >>> x['styles.txt'].parent = x['resources'] 567 | >>> x.seedir() 568 | myfakedir/ 569 | ├─__init__.py 570 | ├─main.py 571 | └─resources/ 572 | ├─images/ 573 | │ └─c.png 574 | └─styles.txt 575 | 576 | ``` 577 | 578 | ### Turn existing directories into fakes 579 | 580 | The `seedir.fakedir.fakedir()` function allows you to convert real directories into `seedir.fakedir.FakeDir` objects: 581 | 582 | ```python 583 | >>> f = sd.fakedir(path) # using path from above 584 | >>> f.seedir() 585 | exampledir/ 586 | ├─jowly.pdf 587 | ├─monkish.txt 588 | ├─pedantic/ 589 | │ └─cataclysmic.txt 590 | ├─scrooge/ 591 | │ ├─light.pdf 592 | │ ├─paycheck/ 593 | │ │ └─electrophoresis.txt 594 | │ └─reliquary.pdf 595 | └─Vogel.txt 596 | 597 | >>> type(f) 598 | 599 | 600 | ``` 601 | 602 | Similar to `seedir.realdir.seedir()`, `seedir.fakedir.fakedir()` has options to limit the incoming folders and files: 603 | 604 | ```python 605 | >>> f = sd.fakedir(path, exclude_folders='scrooge', exclude_files='Vogel.txt') 606 | >>> f.seedir() 607 | exampledir/ 608 | ├─jowly.pdf 609 | ├─monkish.txt 610 | └─pedantic/ 611 | └─cataclysmic.txt 612 | 613 | ``` 614 | 615 | Fake directories created from your system directories can be combined with other ones created from scratch: 616 | 617 | ```python 618 | >>> f.parent = x 619 | >>> x.seedir() 620 | myfakedir/ 621 | ├─__init__.py 622 | ├─main.py 623 | ├─resources/ 624 | │ ├─images/ 625 | │ │ └─c.png 626 | │ └─styles.txt 627 | └─exampledir/ 628 | ├─jowly.pdf 629 | ├─monkish.txt 630 | └─pedantic/ 631 | └─cataclysmic.txt 632 | 633 | ``` 634 | 635 | ### Creating folder trees from text 636 | 637 | The `seedir.fakedir.fakedir_fromstring()` method allows you to read in existing folder tree diagrams. Given this: 638 | 639 | ```python 640 | somedir/ 641 | - folder1 642 | - folder2 643 | - folder3 644 | - file1 645 | - file2 646 | - subfolder 647 | - subfolder 648 | - deepfile 649 | - folder4 650 | ``` 651 | 652 | You can do: 653 | 654 | ```python 655 | >>> s = '''somedir/ 656 | ... - folder1 657 | ... - folder2 658 | ... - folder3 659 | ... - file1 660 | ... - file2 661 | ... - subfolder 662 | ... - subfolder 663 | ... - deepfile 664 | ... - folder4''' 665 | 666 | >>> g = sd.fakedir_fromstring(s) 667 | >>> g.seedir() 668 | somedir/ 669 | ├─folder1 670 | ├─folder2 671 | ├─folder3/ 672 | │ ├─file1 673 | │ ├─file2 674 | │ └─subfolder/ 675 | │ └─subfolder/ 676 | │ └─deepfile 677 | └─folder4 678 | 679 | ``` 680 | 681 | This can be used to read in examples found online. [For instance](https://chrisyeh96.github.io/2017/08/08/definitive-guide-python-imports.html): 682 | 683 | ```python 684 | >>> s = '''test/ # root folder 685 | ... packA/ # package packA 686 | ... subA/ # subpackage subA 687 | ... __init__.py 688 | ... sa1.py 689 | ... sa2.py 690 | ... __init__.py 691 | ... a1.py 692 | ... a2.py 693 | ... packB/ # package packB (implicit namespace package) 694 | ... b1.py 695 | ... b2.py 696 | ... math.py 697 | ... random.py 698 | ... other.py 699 | ... start.py''' 700 | 701 | >>> h = sd.fakedir_fromstring(s, parse_comments=True) # handling the added comments 702 | >>> h.seedir(style='emoji') 703 | 📁 test/ 704 | ├─📁 packA/ 705 | │ ├─📁 subA/ 706 | │ │ ├─📄 __init__.py 707 | │ │ ├─📄 sa1.py 708 | │ │ └─📄 sa2.py 709 | │ ├─📄 __init__.py 710 | │ ├─📄 a1.py 711 | │ └─📄 a2.py 712 | ├─📁 packB/ 713 | │ ├─📄 b1.py 714 | │ └─📄 b2.py 715 | ├─📄 math.py 716 | ├─📄 random.py 717 | ├─📄 other.py 718 | └─📄 start.py 719 | 720 | ``` 721 | 722 | This function has not been extensively tested, so expect failures/misreadings! 723 | 724 | ### Creating random directories 725 | 726 | You can also use seedir to create randomly generated directories. 727 | 728 | ```python 729 | >>> r = sd.randomdir(seed=4.21, extensions=['png', 'jpg', 'pdf'], name='sorandom') 730 | >>> r.seedir() 731 | sorandom/ 732 | ├─plethora.pdf 733 | ├─randy.jpg 734 | ├─giddap.pdf 735 | ├─adage/ 736 | └─Gobi/ 737 | ├─inventive.pdf 738 | ├─Noel.jpg 739 | ├─archaic/ 740 | │ └─bream.pdf 741 | ├─salve/ 742 | └─NY/ 743 | └─Bahrein.jpg 744 | 745 | ``` 746 | 747 | Additional parameters dictate the size of the tree created: 748 | 749 | ```python 750 | >>> sd.randomdir(files=range(5), # making an unnecessarily large tree 751 | ... folders=[1,2], 752 | ... stopchance=.3, 753 | ... depth=5, 754 | ... extensions=['png', 'jpg', 'pdf'], 755 | ... seed=4.21).seedir() 756 | MyFakeDir/ 757 | ├─plethora.pdf 758 | ├─randy.jpg 759 | ├─giddap.pdf 760 | ├─adage.jpg 761 | ├─oint/ 762 | │ ├─inventive.pdf 763 | │ ├─Noel.jpg 764 | │ ├─archaic.pdf 765 | │ ├─NY/ 766 | │ │ ├─neonatal.png 767 | │ │ ├─pyknotic.png 768 | │ │ ├─wife.png 769 | │ │ ├─Mickelson/ 770 | │ │ │ └─Caruso/ 771 | │ │ │ ├─royal.pdf 772 | │ │ │ ├─aurora.pdf 773 | │ │ │ ├─stampede.pdf 774 | │ │ │ ├─divan.pdf 775 | │ │ │ └─Coffey/ 776 | │ │ │ ├─sad.pdf 777 | │ │ │ ├─cob.pdf 778 | │ │ │ ├─natural.jpg 779 | │ │ │ ├─skinflint.pdf 780 | │ │ │ └─exploration/ 781 | │ │ └─Westchester/ 782 | │ │ ├─metamorphism.jpg 783 | │ │ ├─Pulitzer.jpg 784 | │ │ ├─permissive.png 785 | │ │ ├─wakerobin/ 786 | │ │ │ ├─talon.jpg 787 | │ │ │ ├─peace.pdf 788 | │ │ │ ├─drowse.jpg 789 | │ │ │ ├─rutabaga/ 790 | │ │ │ └─silage/ 791 | │ │ │ ├─pert.png 792 | │ │ │ ├─defensible.png 793 | │ │ │ ├─lexicon.png 794 | │ │ │ ├─eureka.pdf 795 | │ │ │ └─impasse/ 796 | │ │ └─calve/ 797 | │ │ ├─superstitious.png 798 | │ │ └─embroider/ 799 | │ │ ├─safekeeping.pdf 800 | │ │ ├─brig.png 801 | │ │ ├─systemic.pdf 802 | │ │ ├─caddis.png 803 | │ │ └─borosilicate/ 804 | │ └─Pentecost/ 805 | │ ├─visceral.pdf 806 | │ ├─replica.png 807 | │ ├─rockaway.jpg 808 | │ ├─luxe.jpg 809 | │ └─breath/ 810 | │ ├─contain.jpg 811 | │ └─homophobia/ 812 | │ ├─crewman.jpg 813 | │ └─restroom/ 814 | └─joss/ 815 | ├─Technion.jpg 816 | ├─Mynheer.png 817 | ├─begonia/ 818 | └─Djakarta/ 819 | ├─Lionel.png 820 | ├─Cantonese.pdf 821 | ├─drafty.jpg 822 | ├─half/ 823 | │ └─builtin/ 824 | │ ├─ermine.jpg 825 | │ └─Cretan/ 826 | │ ├─happenstance.pdf 827 | │ ├─insoluble.jpg 828 | │ ├─Audrey/ 829 | │ └─octennial/ 830 | └─recipient/ 831 | ├─splintery.jpg 832 | ├─bottom.png 833 | ├─stunk.png 834 | ├─Polyhymnia/ 835 | └─Holocene/ 836 | ├─plaque.pdf 837 | ├─westerly.png 838 | ├─glassine.jpg 839 | ├─titanium.pdf 840 | └─square/ 841 | 842 | ``` 843 | 844 | ### Turning fake directories into real ones 845 | 846 | Finally, there is a `seedir.fakedir.FakeDir.realize()` method which can covert fake directories into real folders on your computer. 847 | 848 | ``` 849 | x = sd.randomdir() 850 | x.realize(path='where/to/create/it/') 851 | ``` 852 | 853 | All files created will be empty. 854 | 855 | ## Extending seedir 856 | 857 | It is also possible to use seedir to generate folder tree diagrams for other types of Python objects that have directory-like structures. This can be done by **subclassing the `seedir.folderstructure.FolderStructure` class**, which implements the main algorithm: 858 | 859 | ```python 860 | >>> from seedir.folderstructure import FolderStructure 861 | 862 | ``` 863 | 864 | FolderStructure is an abstract class, which requires three arguments to be implemented: 865 | 866 | 1. `seedir.folderstructure.FolderStructure.getname()`: returns a string name for the object 867 | 2. ``seedir.folderstructure.FolderStructure.isdir()`: returns a boolean indicating whether the object passed is a folder or not. 868 | 3. `seedir.folderstructure.FolderStructure.listdir()`: when called on the folder-like object, returns a list of child objects 869 | 870 | Once these methods are implemented, your custom FolderStructure can be called on inputs to create folder structures. The following example demonstrates creating a "folder" structure for factorizing numbers: 871 | 872 | ```python 873 | >>> def factors(n): 874 | ... return [i for i in range(2, n) if n % i == 0] 875 | 876 | # note that the isdir_func always returns true, treating all numbers as "folders" 877 | >>> class FactorStructure(FolderStructure): 878 | ... 879 | ... def getname(self, item): 880 | ... return str(item) 881 | ... 882 | ... def isdir(self, item): 883 | ... return True 884 | ... 885 | ... def listdir(self, item): 886 | ... return factors(item) 887 | 888 | >>> factorizer = FactorStructure() 889 | >>> factorizer(36, folderend='') 890 | 36 891 | ├─2 892 | ├─3 893 | ├─4 894 | │ └─2 895 | ├─6 896 | │ ├─2 897 | │ └─3 898 | ├─9 899 | │ └─3 900 | ├─12 901 | │ ├─2 902 | │ ├─3 903 | │ ├─4 904 | │ │ └─2 905 | │ └─6 906 | │ ├─2 907 | │ └─3 908 | └─18 909 | ├─2 910 | ├─3 911 | ├─6 912 | │ ├─2 913 | │ └─3 914 | └─9 915 | └─3 916 | 917 | ``` 918 | 919 | -------------------------------------------------------------------------------- /seedir/folderstructure.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The primary algorithm for determining the folder structure returned by 4 | `seedir.realdir.seedir()` and `seedir.fakedir.FakeDir.seedir()`. 5 | 6 | """ 7 | 8 | from abc import ABC, abstractmethod 9 | import copy 10 | import math 11 | import os 12 | 13 | import natsort 14 | 15 | import seedir.printing as printing 16 | 17 | def listdir_fullpath(path): 18 | '''Like `os.listdir()`, but returns absolute paths.''' 19 | return [os.path.join(path, f) for f in os.listdir(path)] 20 | 21 | class FolderStructureArgs: 22 | 23 | def __init__(self, extend='│ ', space=' ', split='├─', final='└─', 24 | filestart='', folderstart='', fileend='', folderend='/', 25 | depthlimit=None, itemlimit=None, beyond=None, first=None, 26 | sort=True, sort_reverse=False, sort_key=None, 27 | include_folders=None, exclude_folders=None, 28 | include_files=None, exclude_files=None, 29 | regex=False, mask=None, formatter=None, 30 | sticky_formatter=False, acceptable_listdir_errors=None, 31 | denied_string=' [ACCESS DENIED]'): 32 | self.extend = extend 33 | self.space = space 34 | self.split = split 35 | self.final = final 36 | self.filestart = filestart 37 | self.folderstart = folderstart 38 | self.fileend = fileend 39 | self.folderend = folderend 40 | self.depthlimit = depthlimit 41 | self.itemlimit = itemlimit 42 | self.beyond = beyond 43 | self.first = first 44 | self.sort = sort 45 | self.sort_reverse = sort_reverse 46 | self.sort_key = sort_key 47 | self.include_folders = include_folders 48 | self.exclude_folders = exclude_folders 49 | self.include_files = include_files 50 | self.exclude_files = exclude_files 51 | self.regex = regex 52 | self.mask = mask 53 | self.formatter = formatter 54 | self.sticky_formatter = sticky_formatter 55 | self.acceptable_listdir_errors = acceptable_listdir_errors 56 | self.denied_string = denied_string 57 | 58 | def copy(self): 59 | return copy.deepcopy(self) 60 | 61 | def update_with_formatter(self, formatter, item): 62 | newstyle = formatter(item) 63 | if newstyle is None: 64 | return 65 | for k, v in newstyle.items(): 66 | setattr(self, k, v) 67 | 68 | class FolderStructure(ABC): 69 | '''General class for determining folder strctures. Implements 70 | the seedir folder-tree generating algorithm over arbitrary objects. 71 | 72 | **This is an abstract class that cannot be instantiated directly.** It must 73 | be subclassed, and the user must imeplement three abstract methods: 74 | 75 | - `getname(self, item)`: Return the string name of a folder or file object. 76 | - `isdir(self, item)`: Return a boolean indicating whether an object is 77 | a folder (i.e., can be further travesed) or not. 78 | - `listdir(self, item)`: For a folder-like object, return its children, 79 | such that these same three methods can be called on each child again 80 | (recursively). 81 | 82 | Details on each of these functions are provided below. In each case, 83 | `item` is a folder-like or file-like object that can be listed in the 84 | diagram. 85 | 86 | Seedir provides implementations for string paths (`RealDirStructure`), 87 | pathlib objects (`PathibStucture`), and "fakedir" objects (`FakeDirStucture`). 88 | 89 | *Note: Prior to v0.5.0, you could initialize a FolderStructure by passing 90 | these three functions as arguments to the constructor. This is now 91 | deprecated and will throw an error.* 92 | ''' 93 | 94 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 95 | # ABSTRACT METHODS WHICH REQUIRE IMPLEMENTATION 96 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 97 | 98 | @abstractmethod 99 | def getname(self, item): 100 | ''' 101 | Generate a string name for an item to put in the tree diagram. 102 | 103 | Parameters 104 | ---------- 105 | item : user-specified 106 | Folder-like or file-like object. 107 | 108 | Returns 109 | ------- 110 | name : str 111 | Name to show in the diagram. 112 | 113 | ''' 114 | ... 115 | 116 | @abstractmethod 117 | def isdir(self, item): 118 | ''' 119 | Returns True if an object is folder-like. 120 | 121 | This function determines whether to treat objects as a folder or a 122 | file. Some key differences of note are: 123 | 124 | - Folders **will** be passed to `listdir()` to retrieve their 125 | children. Files are instead not passed to this function. 126 | - Some arguments are unique to folders or files, e.g. 127 | `exclude_folders` and `exclude_files`. 128 | - Folders and files are (by default) represented differently 129 | in diagrams, with folders usually ending with a slash. 130 | 131 | Parameters 132 | ---------- 133 | item : user-specified 134 | Folder-like or file-like object. 135 | 136 | Returns 137 | ------- 138 | result : bool 139 | True if item is folder; False if not. 140 | 141 | ''' 142 | ... 143 | 144 | @abstractmethod 145 | def listdir(self, item): 146 | ''' 147 | Return the children of a folder-type object. 148 | 149 | Children should always be more objects which can be passed to 150 | `getname()`, `isdir()`, or (in the case of folder-like objects) 151 | `listdir()`. 152 | 153 | This function is not called on objects which are not folders, 154 | as determined by `isdir()`. 155 | 156 | Parameters 157 | ---------- 158 | item : user-specified 159 | Folder-like or file-like object. 160 | 161 | Returns 162 | ------- 163 | children : list 164 | List of child objects for further traversal. Can be empty 165 | for a folder with no children. 166 | 167 | ''' 168 | ... 169 | 170 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 171 | 172 | def __call__(self, folder, style='lines', printout=True, indent=2, uniform=None, 173 | anystart=None, anyend=None, depthlimit=None, itemlimit=None, 174 | beyond=None, first=None, sort=False, sort_reverse=False, 175 | sort_key=None, include_folders=None, exclude_folders=None, 176 | include_files=None, exclude_files=None, regex=False, mask=None, 177 | formatter=None, sticky_formatter=False, 178 | acceptable_listdir_errors=None, denied_string='', **kwargs): 179 | '''Call this on a folder object to generate the seedir output 180 | for that object.''' 181 | 182 | accept_kwargs = ['extend', 'split', 'space', 'final', 183 | 'folderstart', 'filestart', 'folderend', 'fileend'] 184 | 185 | if any(i not in accept_kwargs for i in kwargs.keys()): 186 | bad_kwargs = [i for i in kwargs.keys() if i not in accept_kwargs] 187 | raise ValueError(f'kwargs must be any of {accept_kwargs}; ' 188 | f'unrecognized kwargs: {bad_kwargs}') 189 | 190 | styleargs = printing.get_styleargs(style) 191 | printing.format_indent(styleargs, indent=indent) 192 | 193 | if uniform is not None: 194 | for arg in ['extend', 'split', 'final', 'space']: 195 | styleargs[arg] = uniform 196 | 197 | if anystart is not None: 198 | styleargs['folderstart'] = anystart 199 | styleargs['filestart'] = anystart 200 | 201 | if anyend is not None: 202 | styleargs['folderend'] = anyend 203 | styleargs['fileend'] = anyend 204 | 205 | for k in kwargs: 206 | if k in styleargs: 207 | styleargs[k] = kwargs[k] 208 | 209 | if sort_key is not None or sort_reverse == True: 210 | sort = True 211 | 212 | if acceptable_listdir_errors is None: 213 | acceptable_listdir_errors = tuple() 214 | 215 | args = FolderStructureArgs(depthlimit=depthlimit, 216 | itemlimit=itemlimit, 217 | beyond=beyond, 218 | first=first, 219 | sort=sort, 220 | sort_reverse=sort_reverse, 221 | sort_key=sort_key, 222 | include_folders=include_folders, 223 | exclude_folders=exclude_folders, 224 | include_files=include_files, 225 | exclude_files=exclude_files, 226 | regex=regex, 227 | mask=mask, 228 | formatter=formatter, 229 | sticky_formatter=sticky_formatter, 230 | acceptable_listdir_errors=acceptable_listdir_errors, 231 | denied_string=denied_string, 232 | **styleargs) 233 | 234 | 235 | s = self._folder_structure_recurse(ITEM=folder, 236 | FSARGS=args).strip() 237 | 238 | if printout: 239 | print(s) 240 | else: 241 | return s 242 | 243 | def apply_itemlimit(self, items, itemlimit): 244 | ''' 245 | Limit the number of children in a folder. 246 | 247 | Parameters 248 | ---------- 249 | items : list 250 | Child items. 251 | itemlimit : int, tuple, or None 252 | Limit. If int, limits the number of items regardless of if they 253 | are folders or file. If a 2-tuple, the first number is treated 254 | as a limit on the number of folders and the second number 255 | is treated as a limit on the number of files. If None, 256 | no limit is applied. 257 | 258 | Returns 259 | ------- 260 | finalitems : list 261 | Kept times. 262 | rem : list 263 | Omitted items. 264 | 265 | ''' 266 | 267 | # all items included when None 268 | if itemlimit is None: 269 | itemlimit = len(items) 270 | 271 | # if int, take the first itemlimit items 272 | if isinstance(itemlimit, int): 273 | itemlimit = min(itemlimit, len(items)) 274 | finalitems = items[:itemlimit] 275 | rem = items[itemlimit:] 276 | 277 | # if tuple, interpret as limits for folders and files 278 | else: 279 | finalitems, rem = self._apply_tuple_itemlimit(items, itemlimit) 280 | 281 | return finalitems, rem 282 | 283 | def _apply_tuple_itemlimit(self, items, itemlimit): 284 | '''Apply the itemlimit when it is a 2-tuple of 285 | (folderlimit, filelimit).''' 286 | folderlimit, filelimit = itemlimit 287 | 288 | folderlimit = math.inf if folderlimit is None else folderlimit 289 | filelimit = math.inf if filelimit is None else filelimit 290 | 291 | finalitems = [] 292 | rem = [] 293 | foldercount = 0 294 | filecount = 0 295 | 296 | for item in items: 297 | isdir = self.isdir(item) 298 | 299 | if isdir and (foldercount < folderlimit): 300 | finalitems.append(item) 301 | foldercount += 1 302 | elif isdir: 303 | rem.append(item) 304 | elif not isdir and (filecount < filelimit): 305 | finalitems.append(item) 306 | filecount += 1 307 | else: 308 | rem.append(item) 309 | 310 | return finalitems, rem 311 | 312 | def beyond_depth_str(self, items, beyond): 313 | ''' 314 | Generates the text when using the 'beyond' 315 | parameter and ther are more items than the itemlimit or contents 316 | beyond the depthlimit. 317 | 318 | Parameters 319 | ---------- 320 | beyond : str 321 | Style of beyond string to generate. Options are: 322 | - 'ellipsis' ('...') 323 | - 'content' or 'contents' (the number of files and folders beyond) 324 | - a string starting with '_' (everything after the leading 325 | underscore will be returned) 326 | items : collection of file paths, optional 327 | Path items of the items beyond the limit, used when the 'beyond' 328 | argeument is 'content' or 'contents'. The default is None. 329 | 330 | 331 | Returns 332 | ------- 333 | str 334 | String indicating what lies beyond 335 | 336 | ''' 337 | if beyond.lower() == 'ellipsis': 338 | return '...' 339 | elif beyond.lower() in ['contents','content']: 340 | folders = self.count_folders(items) 341 | files = self.count_files(items) 342 | return '{} folder(s), {} file(s)'.format(folders, files) 343 | elif beyond and beyond[0] == '_': 344 | return beyond[1:] 345 | else: 346 | s1 = '"beyond" must be "ellipsis", "content", or ' 347 | s2 = 'a string starting with "_"' 348 | raise ValueError(s1 + s2) 349 | 350 | def count_files(self, items): 351 | ''' 352 | Count the number of files in a collection of paths. 353 | 354 | Parameters 355 | ---------- 356 | paths : list-like 357 | Collection of paths. 358 | 359 | Returns 360 | ------- 361 | files : int 362 | Count of files. 363 | 364 | ''' 365 | files = sum([not self.isdir(i) for i in items]) 366 | return files 367 | 368 | def count_folders(self, items): 369 | ''' 370 | Count the number of folders in a collection of paths. 371 | 372 | Parameters 373 | ---------- 374 | paths : list-like 375 | Collection of paths. 376 | 377 | Returns 378 | ------- 379 | files : int 380 | Count of folders. 381 | 382 | ''' 383 | folders = sum([self.isdir(i) for i in items]) 384 | return folders 385 | 386 | def filter_items(self, listdir, include_folders=None, 387 | exclude_folders=None, include_files=None, 388 | exclude_files=None, regex=False, mask=None): 389 | 390 | ''' 391 | Filter for folder and file names in folder structures. Removes or includes 392 | items matching filtering strings. 393 | 394 | Note the following priority of arguments: 395 | 1. Mask (totally overwrites include/exclude) 396 | 2. Include (saves items from being excluded) 397 | 3. Exclude 398 | 399 | Parameters 400 | ---------- 401 | listdir : list-like 402 | Collection of file/folder items. 403 | include_folders : str or list-like, optional 404 | Folder names to include. The default is None. 405 | exclude_folders : str or list-like, optional 406 | Folder names to exclude. The default is None. 407 | include_files : str or list-like, optional 408 | File names to include. The default is None. 409 | exclude_files : str or list-like, optional 410 | File names to exclude. The default is None. 411 | regex : bool, optional 412 | Interpret strings as regular expressions. The default is False. 413 | mask : function, optional 414 | Function for filtering items. Absolute paths of each individual item 415 | are passed to the mask function. If True is returned, the 416 | item is kept. The default is None. 417 | 418 | Returns 419 | ------- 420 | keep : list 421 | Filtered input. 422 | 423 | ''' 424 | 425 | def _should_convert(x): 426 | return isinstance(x, str) or x is None 427 | 428 | filtered = [] 429 | for item in listdir: 430 | 431 | name = self.getname(item) 432 | 433 | if self.isdir(item): 434 | inc = [include_folders] if _should_convert(include_folders) else include_folders 435 | exc = [exclude_folders] if _should_convert(exclude_folders) else exclude_folders 436 | else: 437 | inc = [include_files] if _should_convert(include_files) else include_files 438 | exc = [exclude_files] if _should_convert(exclude_files) else exclude_files 439 | 440 | # 1. check mask - which trumps include/exclude arguments 441 | if mask is not None: 442 | if mask(item): 443 | filtered.append(item) 444 | continue 445 | 446 | # set default keep behavior 447 | # items are exluded if inclusion is passed 448 | keep = all([i is None for i in inc]) 449 | 450 | # 2. apply exclusion 451 | for pat in exc: 452 | if pat is not None: 453 | match = printing.is_match(pattern=pat, string=name, regex=regex) 454 | if match: 455 | keep = False 456 | 457 | # 3. apply inclusion (trumps exclusion) 458 | for pat in inc: 459 | if pat is not None: 460 | match = printing.is_match(pattern=pat, string=name, regex=regex) 461 | if match: 462 | keep = True 463 | 464 | if keep: 465 | filtered.append(item) 466 | 467 | return filtered 468 | 469 | def _folder_structure_recurse(self, ITEM, FSARGS, DEPTH=0, 470 | INDEX=0, INCOMPLETE=None, 471 | IS_LASTITEM=False, 472 | IS_RAWSTRING=False): 473 | 474 | # initialization 475 | OUTPUT = '' 476 | 477 | if INCOMPLETE is None: 478 | INCOMPLETE = [] 479 | 480 | # set some variables 481 | is_rootitem = DEPTH == 0 482 | is_lastitem = IS_LASTITEM 483 | is_rawstring = IS_RAWSTRING 484 | 485 | # APPLY FORMATTER 486 | # # # # # # # # # # # # # # 487 | args = FSARGS.copy() 488 | next_args = FSARGS 489 | 490 | if not is_rawstring and args.formatter is not None: 491 | args.update_with_formatter(args.formatter, ITEM) 492 | 493 | if args.sticky_formatter: 494 | next_args = args 495 | 496 | # GET CHILDREN 497 | # # # # # # # # # # # # # # 498 | 499 | error_listing = False 500 | 501 | if not is_rawstring and self.isdir(ITEM): 502 | try: 503 | listdir = self.listdir(ITEM) 504 | except args.acceptable_listdir_errors: 505 | error_listing = True 506 | listdir = None 507 | else: 508 | listdir = None 509 | 510 | # ADD CURRENT ITEM TO OUTPUT 511 | # # # # # # # # # # # # # # 512 | 513 | # create header 514 | base_header = self.get_base_header(INCOMPLETE, 515 | args.extend, 516 | args.space) 517 | 518 | # handle ultimate token in header 519 | if is_rootitem: 520 | branch = '' 521 | elif is_lastitem: 522 | branch = args.final 523 | else: 524 | branch = args.split 525 | 526 | header = base_header + branch 527 | 528 | # start / end tokens 529 | if is_rawstring: 530 | fkey = 'file' 531 | else: 532 | fkey = 'folder' if self.isdir(ITEM) else 'file' 533 | 534 | start = f'{fkey}start' 535 | end = f'{fkey}end' 536 | 537 | # add current item to string 538 | name = self.getname(ITEM) if not is_rawstring else ITEM 539 | error_tag = args.denied_string if error_listing else '' 540 | 541 | OUTPUT += (header + 542 | getattr(args, start) + 543 | name + 544 | getattr(args, end) + 545 | error_tag + 546 | '\n') 547 | 548 | if is_lastitem and INCOMPLETE: 549 | INCOMPLETE.remove(DEPTH-1) 550 | 551 | # EXIT IF NOT FOLDER 552 | # # # # # # # # # # # # # # 553 | 554 | if listdir is None: 555 | return OUTPUT 556 | 557 | # FILTER/SORT CHILDREN 558 | # # # # # # # # # # # # # 559 | 560 | current_itemlimit = args.itemlimit 561 | 562 | # handle when depthlimit is reached 563 | if isinstance(args.depthlimit, int) and DEPTH >= args.depthlimit: 564 | if args.beyond is None: 565 | return OUTPUT 566 | else: 567 | current_itemlimit = 0 568 | 569 | # sort and filter the contents of listdir 570 | sortargs = { 571 | 'first': args.first, 572 | 'sort_reverse': args.sort_reverse, 573 | 'sort_key': args.sort_key} 574 | 575 | filterargs = { 576 | 'include_folders': args.include_folders, 577 | 'exclude_folders': args.exclude_folders, 578 | 'include_files' : args.include_files, 579 | 'exclude_files': args.exclude_files, 580 | 'mask': args.mask, 581 | } 582 | 583 | if args.sort or args.first is not None: 584 | listdir = self.sort_dir(listdir, **sortargs) 585 | 586 | if any(arg is not None for arg in filterargs.values()): 587 | listdir = self.filter_items(listdir, **filterargs, regex=args.regex) 588 | 589 | # apply itemlimit 590 | finalitems, rem = self.apply_itemlimit(listdir, current_itemlimit) 591 | 592 | # append beyond string if being used 593 | beyond_added = False 594 | if args.beyond is not None: 595 | if rem or (DEPTH == args.depthlimit): 596 | finalitems += [self.beyond_depth_str(rem, args.beyond)] 597 | beyond_added = True 598 | 599 | # RECURSE 600 | # # # # # # # # # # # # # # 601 | 602 | if finalitems: 603 | INCOMPLETE.append(DEPTH) 604 | 605 | total = len(finalitems) 606 | 607 | for i, x in enumerate(finalitems): 608 | last = i == (total - 1) 609 | IS_RAWSTRING = (beyond_added and last) 610 | OUTPUT += self._folder_structure_recurse(x, DEPTH=DEPTH+1, 611 | INCOMPLETE=INCOMPLETE, 612 | FSARGS=next_args, 613 | INDEX=i, 614 | IS_LASTITEM=last, 615 | IS_RAWSTRING=IS_RAWSTRING) 616 | 617 | 618 | # RETURN 619 | # # # # # # # # # # # # # # 620 | return OUTPUT 621 | 622 | def get_base_header(self, incomplete, extend, space): 623 | ''' 624 | For folder structures, generate the combination of extend and space 625 | tokens to prepend to file names when generating folder diagrams. 626 | 627 | 628 | The string generated here will still be missing the branch token 629 | (split or final) as well as any folderstart or filestart tokens. 630 | They are added within seedir.seedir(). 631 | 632 | For any item included in a folder diagram, the combination of 633 | extend and space tokens is based on the depth of the item as well as the 634 | parent folders to that item which are not completed. This information 635 | is symbolized with the `incomplete` argument. The following illustrates 636 | the incomplete arguments passed to this function for an example folder 637 | tree: 638 | 639 | ``` 640 | doc/ 641 | ├─_static/ [0] 642 | │ ├─embedded/ [0, 1] 643 | │ │ ├─deep_file [0, 1, 2] 644 | │ │ └─very/ [0, 1, 2] 645 | │ │ └─deep/ [0, 1, 3] 646 | │ │ └─folder/ [0, 1, 4] 647 | │ │ └─very_deep_file [0, 1, 5] 648 | │ └─less_deep_file [0, 1] 649 | └─index.rst [0] 650 | ``` 651 | 652 | Parameters 653 | ---------- 654 | incomplete : list-like 655 | List of integers denoting the depth of incomplete folders at the time 656 | of constructing the line for a given item. Zero represents being 657 | inside the main folder, with increasing integers meaing increasing 658 | depth. 659 | extend : str 660 | Characters symbolizing the extension of a folder. 661 | space : str 662 | Characters providing the gap between items and earlier parents. 663 | 664 | Returns 665 | ------- 666 | str 667 | Base header string. 668 | 669 | ''' 670 | if not incomplete: 671 | return '' 672 | 673 | base_header = [] 674 | max_i = max(incomplete) 675 | for p in range(max_i): 676 | if p in incomplete: 677 | base_header.append(extend) 678 | else: 679 | base_header.append(space) 680 | return "".join(base_header) 681 | 682 | def sort_dir(self, items, first=None, sort_reverse=False, sort_key=None): 683 | ''' 684 | Sorting function used to sort contents when producing folder diagrams. 685 | 686 | Parameters 687 | ---------- 688 | items : list-like 689 | Collection of folder contents. 690 | first : 'files' or 'folders', optional 691 | Sort either files or folders first. The default is None. 692 | sort_reverse : bool, optional 693 | Reverse the sort applied. The default is False. 694 | sort_key : function, optional 695 | Function to apply to sort the objs by their basename. The function 696 | should take a single argument, of the type expected by 697 | this FolderStucture. 698 | 699 | Returns 700 | ------- 701 | list 702 | Sorted input as a list. 703 | 704 | ''' 705 | if sort_key is None: 706 | key = lambda x : self.getname(x) 707 | else: 708 | key = lambda x: sort_key(self.getname(x)) 709 | 710 | if first in ['folders', 'files']: 711 | folders = [p for p in items if self.isdir(p)] 712 | files = [p for p in items if not self.isdir(p)] 713 | folders = natsort.natsorted(folders, reverse=sort_reverse, key=key) 714 | files = natsort.natsorted(files, reverse=sort_reverse, key=key) 715 | output = folders + files if first == 'folders' else files + folders 716 | elif first is None: 717 | output = list(natsort.natsorted(items, reverse=sort_reverse, key=key)) 718 | else: 719 | raise ValueError("`first` must be 'folders', 'files', or None.") 720 | 721 | return output 722 | 723 | class RealDirStructure(FolderStructure): 724 | """Make folder structures from string paths.""" 725 | 726 | def __init__(self): 727 | super().__init__() 728 | self.slashes = os.sep + '/' + '//' 729 | 730 | def getname(self, item): 731 | return os.path.basename(item.rstrip(self.slashes)) 732 | 733 | def isdir(self, item): 734 | return os.path.isdir(item) 735 | 736 | def listdir(self, item): 737 | return listdir_fullpath(item) 738 | 739 | class PathlibStructure(FolderStructure): 740 | """Make folder structures from pathlib objects.""" 741 | 742 | def getname(self, item): 743 | return item.name 744 | 745 | def isdir(self, item): 746 | return item.is_dir() 747 | 748 | def listdir(self, item): 749 | return list(item.iterdir()) 750 | 751 | class FakeDirStructure(FolderStructure): 752 | """Make `seedir.fakedir.FakeDir` folder structures.""" 753 | 754 | def getname(self, item): 755 | return item.name 756 | 757 | def isdir(self, item): 758 | return item.isdir() 759 | 760 | def listdir(self, item): 761 | return item.listdir() 762 | --------------------------------------------------------------------------------