Преобразовать скрипт Python в каталог с помощью __main__.py - PullRequest
4 голосов
/ 12 июля 2019

Насколько я понимаю, я понимаю систему import в Python, но я все еще чувствую себя потерянным ...

Я хочу изменить файл (который является основной точкой входа моих программ) в каталог, но я не могу запустить импорт успешно

Кажется, я не понимаю, как заставить sys.path соответствовать.

$ cat > prog.py << EOF
> import sys
> pprint(sys.path[0])
> EOF

$ python3 prog.py 
/home/me/pyprogram

$ mkdir prog

$ mv prog.py prog/__main__.py

$ python3 prog
prog

$ mv prog/__main__.py prog/__init__.py

$ python3 prog/__init__.py 
/home/me/pyprogram/prog

, чтобы узнать больше о том, чего я пытаюсь достичь (и, возможно, я неправильно спроектировал свою программу, обратная связь с радостью принята)

$ tree --dirsfirst
.
├── prog
│   ├── data_process.py
│   └── __init__.py
├── destination.py
└── source.py

1 directory, 4 files

$ cat source.py 
def get():
    return 'raw data'

$ cat destination.py 
def put(data):
    print(f"{data} has ',
 '/usr/lib/python37.zip',
 '/usr/lib/python3.7',
 '/usr/lib/python3.7/lib-dynload',
 '/home/me/.local/lib/python3.7/site-packages',
 '/usr/local/lib/python3.7/dist-packages',
 '/usr/lib/python3/dist-packages']
been passed successfully")

$ cat prog/__init__.py 
#!/usr/bin/env python

import os


class Task:
    def __init__(self, func, args=None, kwargs=None):
        self.func = func
        self.args = args if args else []
        self.kwargs = kwargs if kwargs else {}

    def run(self):
        self.func(*self.args, **self.kwargs)


tasks = []

def register_task(args=None, kwargs=None):
    def registerer(func):
        tasks.append(Task(func, args, kwargs))
        return func
    return registerer

for module in os.listdir(os.path.dirname(os.path.abspath(__file__))):
    if module.startswith('_') or module.startswith('.'):
        continue
    __import__(os.path.splitext(module)[0])
del module

for task in tasks:
    task.run()

$ cat prog/data_process.py 
from source import get
from destination import put
from . import register_task

@register_task(kwargs={'replace_with': 'cleaned'})
def process(replace_with):
    raw = get()
    cleaned = raw.replace('raw', replace_with)
    put(cleaned)

$ python3 prog/__init__.py 
Traceback (most recent call last):
  File "prog/__init__.py", line 27, in <module>
    __import__(os.path.splitext(module)[0])
  File "/home/me/pyprogram/prog/data_process.py", line 1, in <module>
    from source import get
ModuleNotFoundError: No module named 'source'

$ mv prog/__init__.py prog/__main__.py

$ python3 prog/
Traceback (most recent call last):
  File "/usr/lib/python3.7/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/lib/python3.7/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "prog/__main__.py", line 27, in <module>
    __import__(os.path.splitext(module)[0])
  File "prog/data_process.py", line 1, in <module>
    from source import get
ModuleNotFoundError: No module named 'source'

Обновление структуры проекта

Я изменил структуру; 1. Поместить все библиотеки в utils. 2. Поместите все проекты в projects (используйте __init__.py, чтобы упростить импорт всех созданных проектов в папку). 3. Основной программный скрипт program.py в верхнем каталоге проекта.

Структура проекта:

$ tree
.
├── utils
│   ├── source.py
│   ├── remote_dest.py
│   ├── local_dest.py
│   └── __init__.py
├── projects
│   ├── process2.py
│   ├── process1.py
│   └── __init__.py
└── program.py

Содержимое библиотек, определенных в каталоге utils:

$ cat utils/source.py
"""
Emulates expensive resource to get,
bringing the need to cache it for all client projects.
"""

import time

class _Cache:
    def __init__(self):
        self.data = None

_cache = _Cache()

def get():
    """
    Exposed source API for getting the data,
    get from remote resource or returns from available cache.
    """
    if _cache.data is None: # As well as cache expiration.
        _cache.data = list(_expensive_get())
    return _cache.data

def _expensive_get():
    """
    Emulate an expensive `get` request,
    prints to console if it was invoked.
    """
    print('Invoking expensive get')
    sample_data = [
        'some random raw data',
        'which is in some raw format',
        'it is so raw that it will need cleaning',
        'but now it is very raw'
    ]
    for row in sample_data:
        time.sleep(1)
        yield row

$ cat utils/remote_dest.py
"""
Emulate limited remote resource.
Use thread and queue to have the data sent in the backround.
"""

import time
import threading
import queue

_q = queue.Queue()

def put(data):
    """
    Exposed remote API `put` method
    """
    _q.put(data)

def _send(q):
    """
    Emulate remote resource,
    prints to console when data is processed.
    """
    while True:
        time.sleep(1)
        data = q.get()
        print(f"Sending {data}")

threading.Thread(target=_send, args=(_q,), daemon=True).start()

$ cat utils/local_dest.py
"""
Emulate second source of data destination.
Allowing to demonstrate need from shared libraries.
"""

import datetime
import os

# Create `out` dir if it doesn't yet exist.
_out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'out')
if not os.path.exists(_out_dir):
    os.makedirs(_out_dir)

def save(data):
    """
    Exposed API to store data locally.
    """
    out_file = os.path.join(_out_dir, 'data.txt')
    with open(out_file, 'a') as f:
        f.write(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {data}\n")

Содержание основного скрипта выполнения программы:

$ cat program.py
#!/usr/bin/env python

import os

class Task:
    """
    Class storing `func` along with its `args` and `kwargs` to be run with.
    """
    def __init__(self, func, args=None, kwargs=None):
        self.func = func
        self.args = args if args else []
        self.kwargs = kwargs if kwargs else {}

    def run(self):
        """
        Executes stored `func` with its arguments.
        """
        self.func(*self.args, **self.kwargs)

    def __repr__(self):
        return f"<Task({self.func.__name__})>"

# List that will store the registered tasks to be executed by the main program.
tasks = []

def register_task(args=None, kwargs=None):
    """
    Registers decorated function along with the passed `args` and `kwargs` in the `tasks` list
    as a `Task` for maintained execution.
    """
    def registerer(func):
        print(f"Appending '{func.__name__}' in {__name__}")
        tasks.append(Task(func, args, kwargs)) # Saves the function as a task.
        print(f"> tasks in {__name__}: {tasks}")
        return func # returns the function untouched.
    return registerer

print(f"Before importing projects as {__name__}. tasks: {tasks}")
import projects
print(f"After importing projects as {__name__}. tasks: {tasks}")

print(f"Iterating over tasks: {tasks} in {__name__}")
while True:
    for task in tasks:
        task.run()
    break # Only run once in the simulation

Содержимое отдельных проектов, определенных в каталоге projects:

$ cat projects/process1.py
"""
Sample project that uses the shared remote resource to get data
and passes it on to another remote resource after processing.
"""

from utils.source import get
from utils.remote_dest import put
from program import register_task

@register_task(kwargs={'replace_with': 'cleaned'})
def process1(replace_with):
    raw = get()
    for record in raw:
        put(record.replace('raw', replace_with))

$ cat projects/process2.py
"""
Sample project that uses the shared remote resource to get data
and saves it locally after processing.
"""

from utils.source import get
from utils.local_dest import save
from program import register_task

@register_task()
def process2():
    raw = get()
    for record in raw:
        save(record.replace('raw', '----'))

Содержимое файла __init__.py в каталоге projects:

$ cat projects/__init__.py
"""
use __init__ file to import all projects
that might have been registered with `program.py` using `register_task`
"""

from . import process1, process2

# TODO: Dynamically import all projects (whether file or directory (as project)) that wil be created in the `projects` directory automatically (ignoring any modules that will start with an `_`)
# Something in the sense of:
# ```
# for module in os.listdir(os.path.dirname(os.path.abspath(__file__))):
#     if module.startswith('_') or module.startswith('.'):
#         continue
#     __import__(os.path.splitext(module)[0])
# ```

Тем не менее, когда я запускаю программу, я вижу это; 1. program.py выполняется дважды (один раз как __main__ и один раз как program). 2. Задачи добавляются (во втором прогоне выполнения). Тем не менее, при переборе задач не найдено ни одной.

$ python3 program.py
Before importing projects as __main__. tasks: []
Before importing projects as program. tasks: []
After importing projects as program. tasks: []
Iterating over tasks: [] in program
Appending 'process1' in program
> tasks in program: [<Task(process1)>]
Appending 'process2' in program
> tasks in program: [<Task(process1)>, <Task(process2)>]
After importing projects as __main__. tasks: []
Iterating over tasks: [] in __main__

Я не понимаю;

  1. Почему основной (program.py) файл выполняется дважды, я подумал, что не может быть циклического импорта, поскольку python кэширует импортированные модули?
    (Я взял идею кругового импорта, используемого в колбах, т.е. app.py импорта routes, models и т. Д., Которые все они импортируют app и используют его для определения функциональности, и app.py импортирует их обратно, чтобы добавить функциональность (поскольку колба работает только app.py))
  2. Почему список tasks пуст после того, как к нему добавлены процессы?

1 Ответ

0 голосов
/ 12 июля 2019

После сравнения моего кругового импорта с приложением на основе колб, которое выполняет циклический импорт, следующим образом

Пример программы колб, использующей круговой импорт

Структура приложения колб

(venv) $ echo $FLASK_APP
mgflask.py

(venv) $ tree
.
├── app
│   ├── models
│   │   ├── __init__.py
│   │   ├── post.py
│   │   └── user.py
│   ├── templates/
│   ├── forms.py
│   ├── __init__.py
│   └── routes.py
├── config.py
└── mgflask.py

(venv) $ cat mgflask.py 
#!/usr/bin/env python

from app import app

# ...

(venv) $ cat app/__init__.py 
from flask import Flask
from config import Config
# ... # config imports

app = Flask(__name__) # <---
# ... # config setup

from . import routes, models, errors # <---

(venv) $ cat app/routes.py 

from flask import render_template, flash, redirect, url_for, request
# ... # import extensions
from . import app, db # <---
from .forms import ... 
from .models import ...

@app.route('/')
def index():
    return render_template('index.html', title='Home')

(venv) $ flask run
 * Serving Flask app "mgflask.py" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: ???-???-???

Я реструктурировал свое приложение:

  1. Я переместил класс Task, список tasks, функцию декоратора register_task в projects/__init__.py и внизу init Файл .py Я импортирую проекты, определенные в каталоге
  2. . В файле program.py я просто from projects import tasks, и все работает как нужно.

Единственный вопрос, который остается:разница между запуском prog.py против prog/ (который содержит __main__.py) (первая итерация моего вопроса здесь ...)

...