Skip to content

Commit ef4e473

Browse files
authored
Merge pull request #11 from pomponchik/develop
0.0.22
2 parents 23899ce + fde8f1e commit ef4e473

File tree

17 files changed

+241
-95
lines changed

17 files changed

+241
-95
lines changed

.codecov.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
threshold: 80%

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ test.py
1010
.coverage.*
1111
tests/cli/data/chpok
1212
tests/cli/data/pok
13+
.idea

README.md

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ Thanks to this package, it is very easy to manage the lifecycle of packages.
2020
## Table of contents
2121

2222
- [**Quick start**](#quick-start)
23+
- [**REPL mode**](#repl-mode)
2324
- [**Script launch mode**](#script-launch-mode)
24-
- [**Special comment language**](#special-comment-language)
25-
- [**Using multiple environments**](#using-multiple-environments)
2625
- [**Context manager mode**](#context-manager-mode)
2726
- [**Installing multiple packages**](#installing-multiple-packages)
2827
- [**Options**](#options)
2928
- [**Using an existing virtual environment**](#using-an-existing-virtual-environment)
3029
- [**Output and logging**](#output-and-logging)
30+
- [**Special comment language**](#special-comment-language)
31+
- [**Using multiple environments**](#using-multiple-environments)
3132
- [**How does it work?**](#how-does-it-work)
3233

3334

@@ -39,14 +40,16 @@ Install [it](https://pypi.org/project/instld/):
3940
pip install instld
4041
```
4142

42-
And use the library in one of two ways: by running your script through it or by importing a context manager from there.
43+
And use the library in one of three ways: by typing commands via REPL, by running your script through it or by importing a context manager from there.
4344

4445
If you run the script [like this](#script-launch-mode), all dependencies will be automatically installed when the application starts and deleted when it stops:
4546

4647
```bash
4748
instld script.py
4849
```
4950

51+
The [REPL mode](#repl-mode) works in a similar way, you just need to type `instld` in the console to enter it.
52+
5053
You can also call the [context manager](#context-manager-mode) from your code:
5154

5255
```python
@@ -59,57 +62,39 @@ with instld('some_package'):
5962
Read more about each method, its capabilities and limitations below.
6063

6164

62-
## Script launch mode
65+
## REPL mode
6366

64-
You can use `instld` to run your script. To do this, you need to run a command like this in the console:
67+
REPL mode is the fastest and easiest way to try out other people's libraries for your code. Just type this in your console:
6568

6669
```bash
67-
instld script.py
70+
instld
6871
```
6972

70-
The contents of the script will be executed in the same way as if you were running it through the `python script.py` command. If necessary, you can pass additional arguments to the command line, as if you are running a regular Python script. However, if your program has imports of any packages other than the built-in ones, they will be installed automatically. Installed packages are automatically cleaned up when you exit the program, so they don't leave any garbage behind.
71-
72-
73-
### Special comment language
74-
75-
When using script launch mode, you can specify additional parameters for each import inside your program. To do this, you need to write immediately after it (but always in the same line!) a comment that starts with "instld:", separating key and value pairs with commas.
76-
77-
As example, if the name of the imported module and the package name are different, this code imports the `f` function from the [`fazy`](https://github.com/pomponchik/fazy) library version `0.0.3`:
78-
79-
```python
80-
import f # instld: version 0.0.3, package fazy
73+
After that you will see a welcome message similar to this:
8174

82-
print(f('some string'))
8375
```
76+
⚡ INSTLD REPL based on
77+
Python 3.11.6 (main, Oct 2 2023, 13:45:54) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin
78+
Type "help", "copyright", "credits" or "license" for more information.
8479
85-
You can also specify only the version or only the package name in the comment, they do not have to be specified together.
80+
>>>
81+
```
8682

83+
Enjoy the regular Python [interactive console mode](https://docs.python.org/3/tutorial/interpreter.html#interactive-mode)! Any libraries that you ask for will be installed within the session, and after exiting it, they will be deleted without a trace. You don't need to "clean up" anything after exiting the console.
8784

88-
### Using multiple environments
85+
In this mode, a [special comment language](#special-comment-language) is fully supported.
8986

90-
The instld script launch mode provides a unique opportunity to use multiple virtual environments at the same time.
87+
## Script launch mode
9188

92-
Firstly, you can run scripts in the main virtual environment, and it will work exactly as you expect:
89+
You can use `instld` to run your script from a file. To do this, you need to run a command like this in the console:
9390

9491
```bash
95-
python3 -m venv venv
96-
source venv/bin/activate
9792
instld script.py
9893
```
9994

100-
When the "import" command is executed in your script, the package will first be searched in the activated virtual environment, and only then downloaded if it is not found there. Note that by default, the activated virtual environment is read-only. That is, it is assumed that you will install all the necessary libraries there before running your script. If you want to install packages in runtime in a specific virtual environment - read about the second method further.
101-
102-
Secondly, you can specify the path to the virtual environment directly [in the comments](#special-comment-language) to a specific import using the `where` directive:
103-
104-
```python
105-
import something # instld: where path/to/the/venv
106-
```
107-
108-
If the path you specified does not exist when you first run the script, it will be automatically created. Libraries installed in this way are not deleted when the script is stopped, therefore, starting from the second launch, the download is no longer required.
95+
The contents of the script will be executed in the same way as if you were running it through the `python script.py` command. If necessary, you can pass additional arguments to the command line, as if you are running a regular Python script. However, if your program has imports of any packages other than the built-in ones, they will be installed automatically. Installed packages are automatically cleaned up when you exit the program, so they don't leave any garbage behind.
10996

110-
Note that the path to the virtual environment in this case should not contain spaces. In addition, there is no multiplatform way to specify directory paths using a comment. Therefore, it is not recommended to use paths consisting of more than one part.
111-
112-
Since script launch mode uses a context manager to install packages "under the hood", you should also read about the features of installing packages in this way in the [corresponding section](#using-an-existing-virtual-environment).
97+
In this mode, as in [REPL](#repl-mode), a [special comment language](#special-comment-language) is fully supported.
11398

11499

115100
## Context manager mode
@@ -174,7 +159,7 @@ with instld('flask==2.0.2') as context_1:
174159
> ⚠️ Keep in mind that although inter-thread isolation is used inside the library, working with contexts is not completely thread-safe. You can write code in such a way that two different contexts import different modules in separate threads at the same time. In this case, you may get paradoxical results. Therefore, it is recommended to additionally isolate with mutexes all cases where you import something from contexts in different threads.
175160
176161

177-
### Options
162+
## Options
178163

179164
You can use [any options](https://pip.pypa.io/en/stable/cli/pip_install/) available for `pip`. To do this, you need to slightly change the name of the option, replacing the hyphens with underscores, and pass it as an argument to `instld`. Here is an example of how using the `--index-url` option will look like:
180165

@@ -284,6 +269,48 @@ with instld('flask', catch_output=True):
284269
The `INFO` [level](https://docs.python.org/3/library/logging.html#logging-levels) is used by default. For errors - `ERROR`.
285270

286271

272+
## Special comment language
273+
274+
When using script launch or REPL mode, you can specify additional parameters for each import inside your program. To do this, you need to write immediately after it (but always in the same line!) a comment that starts with "instld:", separating key and value pairs with commas.
275+
276+
As example, if the name of the imported module and the package name are different, this code imports the `f` function from the [`fazy`](https://github.com/pomponchik/fazy) library version `0.0.3`:
277+
278+
```python
279+
import f # instld: version 0.0.3, package fazy
280+
281+
print(f('some string'))
282+
```
283+
284+
You can also specify only the version or only the package name in the comment, they do not have to be specified together.
285+
286+
287+
## Using multiple environments
288+
289+
The instld script launch mode and REPL mode provides a unique opportunity to use multiple virtual environments at the same time.
290+
291+
Firstly, you can run scripts in the main virtual environment, and it will work exactly as you expect:
292+
293+
```bash
294+
python3 -m venv venv
295+
source venv/bin/activate
296+
instld script.py
297+
```
298+
299+
When the "import" command is executed in your script, the package will first be searched in the activated virtual environment, and only then downloaded if it is not found there. Note that by default, the activated virtual environment is read-only. That is, it is assumed that you will install all the necessary libraries there before running your script. If you want to install packages in runtime in a specific virtual environment - read about the second method further.
300+
301+
Secondly, you can specify the path to the virtual environment directly [in the comments](#special-comment-language) to a specific import using the `where` directive:
302+
303+
```python
304+
import something # instld: where path/to/the/venv
305+
```
306+
307+
If the path you specified does not exist when you first run the script, it will be automatically created. Libraries installed in this way are not deleted when the script is stopped, therefore, starting from the second launch, the download is no longer required.
308+
309+
Note that the path to the virtual environment in this case should not contain spaces. In addition, there is no multiplatform way to specify directory paths using a comment. Therefore, it is not recommended to use paths consisting of more than one part.
310+
311+
Since script launch mode uses a context manager to install packages "under the hood", you should also read about the features of installing packages in this way in the [corresponding section](#using-an-existing-virtual-environment).
312+
313+
287314
## How does it work?
288315

289316
This package is essentially a wrapper for `venv` and `pip`.

instld/cli/main.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
import code
34
import builtins
45
import importlib
56
import inspect
@@ -8,13 +9,16 @@
89
from threading import RLock
910

1011
import instld
11-
from instld.cli.parsing_comments.get_options_from_comments import get_options_from_comments
12+
from instld.cli.parsing_comments.get_options_from_comments import get_options_from_comments_by_frame
1213
from instld.cli.parsing_arguments.get_python_file import get_python_file
1314
from instld.cli.traceback_cutting.cutting import set_cutting_excepthook
15+
from instld.state_management.storage import state_storage, RunType
16+
from instld.errors import CommentFormatError
1417

1518

1619
def main():
1720
python_file = get_python_file()
21+
state_storage.run_type = RunType.script
1822

1923
with instld() as context:
2024
lock = RLock()
@@ -49,21 +53,29 @@ def import_wrapper(name, *args, **kwargs):
4953
last_name = splitted_name[-1]
5054

5155
current_frame = inspect.currentframe()
52-
options = get_options_from_comments(current_frame.f_back)
56+
options = get_options_from_comments_by_frame(current_frame.f_back)
5357

5458
package_name = options.pop('package', base_name)
5559

5660
if 'version' in options:
5761
package_name = f'{package_name}=={options.pop("version")}'
5862

63+
catch_output = options.pop('catch_output', 'no').lower()
64+
if catch_output in ('yes', 'on', 'true'):
65+
catch_output = True
66+
elif catch_output in ('no', 'off', 'false'):
67+
catch_output = False
68+
else:
69+
raise CommentFormatError('For option "catch_output" you can use the following values: "yes", "on", "true", "no", "off", "false".')
70+
5971
current_context = get_current_context(options.pop('where', None))
6072

6173
with lock:
6274
with set_import():
6375
try:
6476
result = __import__(name, *args, **kwargs)
6577
except (ModuleNotFoundError, ImportError) as e:
66-
current_context.install(package_name)
78+
current_context.install(package_name, catch_output=catch_output, **options)
6779
result = current_context.import_here(base_name)
6880
sys.modules[base_name] = result
6981

@@ -78,13 +90,38 @@ def import_wrapper(name, *args, **kwargs):
7890

7991
return result
8092

81-
builtins.__import__ = import_wrapper
93+
if python_file is None:
94+
try:
95+
import readline
96+
except ImportError:
97+
pass
98+
99+
state_storage.run_type = RunType.REPL
100+
builtins.__import__ = import_wrapper
101+
102+
class REPL(code.InteractiveConsole):
103+
def push(self, line):
104+
state_storage.last_string = line
105+
return super().push(line)
106+
107+
108+
banner_strings = [
109+
'⚡ INSTLD REPL based on\n'
110+
'Python %s on %s\n' % (sys.version, sys.platform),
111+
'Type "help", "copyright", "credits" or "license" for more information.\n',
112+
]
113+
banner = ''.join(banner_strings)
114+
115+
REPL().interact(banner=banner)
116+
82117

83-
spec = importlib.util.spec_from_file_location('kek', os.path.abspath(python_file))
84-
module = importlib.util.module_from_spec(spec)
85-
sys.modules['__main__'] = module
86-
set_cutting_excepthook(4)
87-
spec.loader.exec_module(module)
118+
else:
119+
builtins.__import__ = import_wrapper
120+
spec = importlib.util.spec_from_file_location('kek', os.path.abspath(python_file))
121+
module = importlib.util.module_from_spec(spec)
122+
sys.modules['__main__'] = module
123+
set_cutting_excepthook(4)
124+
spec.loader.exec_module(module)
88125

89126

90127
if __name__ == "__main__":

instld/cli/parsing_arguments/get_python_file.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,5 @@
33

44

55
def get_python_file():
6-
if len(sys.argv) < 2:
7-
print('usage: instld python_file.py [argv ...]', file=sys.stderr)
8-
sys.exit(1)
9-
10-
return sys.argv[1]
6+
if len(sys.argv) >= 2:
7+
return sys.argv[1]
Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
from functools import lru_cache
22

33
from instld.errors import InstallingPackageError
4+
from instld.state_management.storage import state_storage, RunType
45

56

7+
def get_comment_substring_from_string(string):
8+
splitted_line = string.split('#')
9+
right_part = splitted_line[1:]
10+
right_part = '#'.join(right_part)
11+
right_part = right_part.strip()
12+
if right_part.startswith('instld:'):
13+
right_part = right_part[7:].strip()
14+
if right_part:
15+
return right_part
16+
else:
17+
raise InstallingPackageError('An empty list of options in the comment.')
18+
619
@lru_cache()
720
def get_comment_string_from_file(line_number, file_name):
821
try:
922
with open(file_name, 'r') as file:
1023
for index, line in enumerate(file):
1124
if index + 1 == line_number:
12-
splitted_line = line.split('#')
13-
right_part = splitted_line[1:]
14-
right_part = '#'.join(right_part)
15-
right_part = right_part.strip()
16-
if right_part.startswith('instld:'):
17-
right_part = right_part[7:].strip()
18-
if right_part:
19-
return right_part
20-
else:
21-
raise InstallingPackageError('An empty list of options in the comment.')
22-
break
25+
return get_comment_substring_from_string(line)
2326

2427
except (FileNotFoundError, OSError):
2528
return None
2629

27-
def get_comment_string(frame):
28-
line_number = frame.f_lineno
29-
code = frame.f_code
30-
file_name = code.co_filename
30+
def get_comment_string_by_frame(frame):
31+
if state_storage.run_type == RunType.script:
32+
line_number = frame.f_lineno
33+
code = frame.f_code
34+
file_name = code.co_filename
35+
36+
return get_comment_string_from_file(line_number, file_name)
3137

32-
return get_comment_string_from_file(line_number, file_name)
38+
elif state_storage.run_type == RunType.REPL:
39+
return get_comment_substring_from_string(state_storage.last_string)

instld/cli/parsing_comments/get_options_from_comments.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from instld.errors import InstallingPackageError
2-
from instld.cli.parsing_comments.get_comment_string import get_comment_string
2+
from instld.cli.parsing_comments.get_comment_string import get_comment_string_by_frame
33

44

5-
def get_options_from_comments(frame):
6-
comment_string = get_comment_string(frame)
7-
5+
def get_options_from_comments(comment_string):
86
result = {}
97

108
if comment_string is not None:
@@ -21,4 +19,11 @@ def get_options_from_comments(frame):
2119
option_value = splitted_option[1].strip().lower()
2220
result[option_name] = option_value
2321

22+
result.pop('doc', None)
23+
result.pop('comment', None)
24+
2425
return result
26+
27+
def get_options_from_comments_by_frame(frame):
28+
comment_string = get_comment_string_by_frame(frame)
29+
return get_options_from_comments(comment_string)

instld/errors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ class RestartingCommandError(Exception):
66

77
class RunningCommandError(Exception):
88
pass
9+
10+
class CommentFormatError(Exception):
11+
pass

instld/module/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ def new_path(self, module_name):
4242
yield
4343
sys.path = old_path
4444

45-
def install(self, *package_names, **options):
45+
def install(self, *package_names, catch_output=False, **options):
4646
if not package_names:
4747
raise ValueError('You need to pass at least one package name.')
4848

4949
options = convert_options(options)
50-
with self.installer(package_names, options=options):
50+
with self.installer(package_names, catch_output=catch_output, options=options):
5151
pass

0 commit comments

Comments
 (0)