astral-sh / uv

An extremely fast Python package and project manager, written in Rust.
https://docs.astral.sh/uv
Apache License 2.0
26.34k stars 766 forks source link

Support .env-files #1384

Open woutervh opened 9 months ago

woutervh commented 9 months ago

In my workflow I make heavy use of env-files.

several tools support .env out of the box:

It would be nice if uv could support .env-files in the $CWD or parent-dirs. see https://crates.io/crates/dotenv

zanieb commented 9 months ago

Thanks for engaging with the project!

Specifically, you would like us to read .env files before every operation? Or something else?

woutervh commented 9 months ago

Specifically, you would like us to read .env files before every operation? yes.

pip supports a config-file pip.ini / pip.conf all those values in that config-support can also be set as environment-variables

I have not yet seen a reference that uv pip supports such config-file or these envvars, but many times I set such project-specific pip-envvars, the same for twine.

some examples:

<project-dir>
   .env
  .venv/
   docs/
   src/
   tests/

so my request is to support .env-files in $CWD, and optionally in parent-levels (e.g in just this is configurable)

woutervh commented 9 months ago

I always try to setup projects in an isolated way:

<project-dir>
   .env
  .venv/
   docs/
   src/
   tests/
   var/cache
   var/cache/ipython
   var/cache/pytest
   var/cache/python
   var/cache/ruff
   var/cache/sphinx
   var/cache   
   var/log
   var/tmp

to make ruff use the my .env to read RUFF_CACHE_DIR, I currently have to install ruff in the .venv and use python-entrypoint to execute ruff (slowing things down)

I use my package https://github.com/libranet/autoread-dotenv to read the .env via sitecustomize-hooks, so the env-vars become available for any python-generated entrypoint.

zanieb commented 4 months ago

I'm going to mark this as not-planned for now. We may revisit in the future. If people have additional use-cases or thoughts please feel free to share!

dustalov commented 3 months ago

I have a similar use case.

I am launching machine learning demos for students, and I am currently using Pipenv which loads environment variables from a file named .env. That file contains variables like DO_NOT_TRACK=1 and HF_HUB_DISABLE_TELEMETRY=1 which are applied for all the commands I run via pipenv run.

As a workaround, I can define them in my shell profile, but it would be great to have it natively supported by uv.

Count-Count commented 2 months ago

I am also migrating from pipenv which uses .env out of the box. It would be great if uv run could support this.

zanieb commented 2 months ago

Willing to consider this further, i.e., we'd read it in uv run only.

patrick91 commented 2 months ago

This might of inspiration, PDM supports something like this: https://pdm-project.org/en/latest/usage/scripts/#env_file

I've personally used this as a way to not use tools like dotenv, since I usually need them locally and not on production 😊 (plus is one less thing to worry about)

lcoleman-scorability commented 2 months ago

We would really love teh ability to load from a .env file for the same reason as above - we use a lot of our applications configuration in .env files.

simonw commented 2 months ago

Supporting this in uv run seems like it could be really useful to me. When I'm working with Django I often have it pick up os.environ["..."] for things like API keys needed by my application. uv run ./manage.py runserver to pick up those from a .env file could be a really nice pattern.

LucasRoesler commented 2 months ago

I am also interested in the uv run support. I am coming from Poetry and currently use this plugin to achieve the same flow to pick up env variables from the .env at the project root.

kamuridesu commented 2 months ago

I think it would be useful to have a flag like --env-file or some section in the config file like PDM does.

But why limit it to uv run only? uv uses a set of environment variables in place of command line arguments, that way you can use different definitions for uv depending on your environment. I personally think it would be useful for situations like when you use a custom packages index for example.

Dufran commented 2 months ago

I am also interested in the uv run support. I am coming from Poetry and currently use this plugin to achieve the same flow to pick up env variables from the .env at the project root.

Hi, I was also migrating from poetry to uv, and currently use taskfile to handle .env variable passing to uv run

Example of the taskfile

version: "3"
dotenv: [".env"]
tasks:
  manage:
    cmds:
      - uv run ./backend/manage.py {{.CLI_ARGS}}

and then i can run stuff like task manage -- makemigraitons task manage -- shell_plus etc

sminnee commented 2 months ago

Another user-case: I supply environment variables to my dev server with a .env file. I've been using poetry run bin/serve-dev to start things with a dotenv loading plugin for poetry.

uv run bin/serve-dev auto-loading my .env file would be nice, but to be honest uv run --env-file .env bin/serve-dev is fine too, if you want to limit the blast radius of this change.

TheRealBecks commented 2 months ago

After the migration from pipenv to uv our local Ansible environments are now broken as uv does not read the .env file.

@zanieb @charliermarsh After reading this thread it looks like that missing feature is a real showstopper?

gusutabopb commented 2 months ago

Here's a non-Python example: Node JS added native support to this feature in September 2023 (changelog) and it has been supported by Bun/Deno for a while now.

petermbauer commented 1 month ago

My use case is to use a separate project-specific Conan Local Cache when entering the respective venv. This simply means setting the CONAN_USER_HOME env var to the venv path with source .venv/bin/activate. Currently, this is done by fumbling this into the generated activate/deactivate scripts. These scripts also vary between the Python versions so this also needs to be tested and updated for new Python versions :-(

cbrnr commented 1 month ago

I'm not sure if my use case is related, but I really enjoy pyenv automatically activating a specific venv if it finds a .python-version file in the directory. I guess the same can be achieved with .env, but maybe there is already a way to use uv to auto-activate envs? Or is this not necessary anymore with uv run (sorry for my ignorance, I haven't really used uv much yet)?

zanieb commented 1 month ago

Or is this not necessary anymore with uv run (sorry for my ignorance, I haven't really used uv much yet)?

It shouldn't be needed if you use uv run, though it's a bit more nuanced than that. Feel free to open a new issue if you run into problems there.

blakeNaccarato commented 1 month ago

I like Brett Cannon's and Jeff Triplett's takes on .env files, which identify the orthogonal purposes of using such files for configuration and for secrets. They also identify the issue with the lack of a standard of .env files, e.g. path separator variation and other nuances.

Brett posed the question, could we standardize on TOML for this? I think if uv picks this up, it could be done in stages, supporting .env today but with no promises on the unstandardized format, then eventually incorporating a more opinionated take inside pyproject.toml in a subtable of tool.uv.

Secrets management is it's own beast, of course.

The upside to uv tackling lockfiles is the standards discussion on that is already pretty mature, but there's no clear consensus on standardizing something for .env, so maybe that's a bridge too far.

woutervh commented 1 month ago

@blakeNaccarato IMHO it is widely understood that .env-files should have a bash-compatible syntax. Besides that, there should not be anything language or tool specific.

sminnee commented 1 month ago

Brett posed the question, could we standardize on TOML for this?

The great thing about ruff is that it's essentially a drop-in replacement for pylint and other tools you were probably using before.

Doing the same with .env files would be a DX win.

Championing an emergent new format would be an interesting idea, as a separate and second step, but until it becomes comparable to .env in terms of adoption, it seems like a DX loss to deprecate .env.

seapagan commented 1 month ago

I use direnv for this exact use-case. It is installed as a global binary on pretty much any OS, then activates any .envrc or optionally also .env when the folder is entered. This is a mature tool that does exactly this one job very well, with the bonus of not being restricted to only Python as it works on any folder, anywhere.

The big plus for me is that you need to explicitly enable the file for each folder (once off check) so it will not activate any random file you get in a cloned repo.

uv doesn't need to do absolutely everything :wink:

petermbauer commented 1 month ago

Unfortunately i have to use Windows so direnv won't work for me.

seapagan commented 1 month ago

Unfortunately i have to use Windows so direnv won't work for me.

I haven't tried it myself, but there is a windows download using winget offered, even though it does say only '*nix' compatible on their github 🤷‍♂️. Ignore me if you've tried this and it didn't work :grin:

EDIT: people have used it with varying degrees of success, this gist has a walk-through

EDIT 2: I had a bit of time so I booted into windows, updated powershell as it doesn't work in the default version, traced and sorted the issues it has by default and jumped through hoops (needs 3 badly documented env var set). Then came to the conclusion it is totally borked under windows. Sorry, carry on :grin:

blakeNaccarato commented 1 month ago

I wrote an isolated script for syncing .env with a tool.dev.env table in pyproject.toml (by default, but it's configurable via CLI) that you can run in a shell profile or in your shell initialization flow. It's barebones and only supports environment variables hard-coded in your pyproject.toml (e.g. no secrets), but I'm sure someone could extend it into a full utility. See also dump-env for a feature set supporting secrets and .env templating.

charliermarsh commented 1 month ago

I’m supportive of adding this to uv run. It would also enforce that the point is not to configure uv but to configure the underlying commands.

(I think it’s not too difficult to add and there’s clearly a lot of demand.)

charliermarsh commented 1 month ago

I think there are two options here:

  1. Respect .env in uv run (but not elsewhere). So, .env would be for providing runtime environment variables, but not for configuring uv itself.
  2. Respect .env everywhere.

My inclination is to do the former (and I think we could get consensus on + ship that quickly). The downside is that you still need to provide UV_-prefixed environment variables (which could include credentials) through some other mechanism.

Maybe we start with (1) and we can always expand to (2) if it's well-motivated.

If we do (1), my preference would be:

(In other words, I think it should be opt-out, not opt-in.)

command-tab commented 1 month ago

For my use cases, option 1 is sufficient. I only need .env files to be loaded at uv run time, and I would prefer uv run do so by default, which is equivalent to pipenv's pipenv run behavior. Offering opt-out flags seems like a good move.

With regard to credentials, which is often my reason for using a .env file in development, could you explain more about this statement?

My inclination is to do the former (and I think we could get consensus on + ship that quickly). The downside is that you still need to provide UV_-prefixed environment variables (which could include credentials) through some other mechanism.

In that scenario, if I had a development credential that was previously kept in an un-committed/.gitignored as, say, GOOGLE_API_KEY=123456 would I need to prefix GOOGLE_API_KEY with UV_ for uv to load it? Or are you talking only about env vars for configuring uv?

charliermarsh commented 1 month ago

Or are you talking only about env vars for configuring uv?

I'm only referring to these. In other words, we'd pass the environment variables onwards to whatever uv run runs, but we wouldn't use them within uv itself.

command-tab commented 1 month ago

Ah, OK. That makes sense, and requiring UV_ vars to be set elsewhere is OK for my needs. If I configure uv, I usually do so with env vars defined in my shell profile, not per-project in a .env file.

seapagan commented 1 month ago

(In other words, I think it should be opt-out, not opt-in.)

I'd prefer the opposite, though I'm probably in the minority. I tend to exclusively use virtualenvs (and with direnv I don't even have to activate them), and a fair percentage of my code tends to run as services or sockets which would stil need a dotenv-type solution so i rarely use uv run even in development. I think making a new feature optional at least at first (if it's configurable by the uv.toml and well documented this should be usable?). More importantly, direnv for example requires explicit permission (once) to load any .env file - i'd not like any repo i cloned to be able to set any ENV var it wants - overwrite PYTHONHOME/PYTHONPATH or system stuff?

sminnee commented 1 month ago

@charliermarsh's recommendation (start with # 1, and make it opt-out) makes a lot of sense to me.

TheRealBecks commented 1 month ago

I think there are two options here:

  1. Respect .env in uv run (but not elsewhere). So, .env would be for providing runtime environment variables, but not for configuring uv itself.
  2. Respect .env everywhere.

My inclination is to do the former (and I think we could get consensus on + ship that quickly). The downside is that you still need to provide UV_-prefixed environment variables (which could include credentials) through some other mechanism.

Maybe we start with (1) and we can always expand to (2) if it's well-motivated.

If we do (1), my preference would be:

  • Always read .env
  • Allow users to pass --env-file (or UV_ENV_FILE) to override the file.
  • Add --no-env-file and UV_NO_ENV_FILE to disable it.

(In other words, I think it should be opt-out, not opt-in.)

Option (2) is exactly what tools like Docker do, so a big 👍 here.

With option (1) we will already get most of the use cases for running local (development) run commands. But option (2) will be needed for e.g. build commands for smaller projects that don't use a CI/CD pipeline, yet. I think that option will also fulfill their needs and the env discussion can be closed as a happy end 😀

IMHO without option 2 that topic will pop up sooner or later again and the discussion begins from the start...

petermbauer commented 1 month ago

With option (1) we will already get most of the use cases for running local (development) run commands. But option (2) will be needed for e.g. build commands for smaller projects that don't use a CI/CD pipeline, yet. I think that option will also fulfill their needs and the env discussion can be closed as a happy end 😀

IMHO without option 2 that topic will pop up sooner or later again and the discussion begins from the start...

Fully agree. uv run should give the same results as executing the command after activating the venv first to not confuse people.

darthtrevino commented 1 month ago

Just to add a thought here, it would be really handy to define a project .env file next to the lockfile that could set the UV_EXTRA_INDEX_URL and UV_KEYRING_PROVIDER variables. This way I could switch between open-source project and work projects without editing my shell environment

woutervh commented 1 month ago

I'm always using .env to set project-specific variables for pip, twine,django,uvicorn ... in each project.

All my projects are isolated application directories (think virtualenv on steroids), where we actively try hard to avoid on anything depending in a users $HOME-dir.
(Avoiding: "it works on my machine" ... because of your .dotfiles)

My first use-cases would be to set the env-variables equivalents of PIP* and "TWINE" for "uv pip install" + "uv publish"
mainly to set credentials that can not be used directly in a pyproject.toml.

For example:

in .env

PERSONAL_ACCESS_TOKEN='<personal-access-token>'
TWINE_REPOSITORY_URL='<url>'
TWINE_PASSWORD='<password>'
TWINE_USERNAME='<username>'

then in pyproject.toml if https://github.com/astral-sh/uv/issues/5734 is implemented

[tool.uv]
index-url = "https://__token__:${PERSONAL_ACCESS_TOKEN}@gitlab.com/..."

Respect .env everywhere.

I'm favor to always read the .env to also support the project-specific UV_*-variables to override the global default config.

Allow users to pass --env-file (or UV_ENV_FILE) to override the file.

Yes.

Add --no-env-file and UV_NO_ENV_FILE to disable it. In other words, I think it should be opt-out, not opt-in.

Yes. opt-out via an externally managed variable makes more sense logically (because all other vars are already managed outside .env) than opt-in (use the .env) via a non-.dotenv managed variable.

charliermarsh commented 3 weeks ago

In https://github.com/astral-sh/uv/pull/8263, some commenters are making the case that this should be opt-in rather than opt-out. I want to open up the conversation for more opinions here before this ships.

joshsleeper commented 3 weeks ago

I personally feel like reading any .env or otherwise named env file by default, without any input or config telling uv run to do so, is undesirable.

I also feel like having simple pyproject.toml config like read_dotenv_files = true (or equivalent CLI option) should opt-in to that behavior being automatic.

that allows projects (or users of projects) to trivially opt-in to dotenv style reading if it's desired/utilized and poses no risk for projects that don't desire to use it.

seapagan commented 3 weeks ago

I personally feel like reading any .env or otherwise named env file by default, without any input or config telling uv run to do so, is undesirable.

I also feel like having simple pyproject.toml config like read_dotenv_files = true (or equivalent CLI option) should opt-in to that behavior being automatic.

Yeah. I commented similar on the other thread and suggested just a global config option but this would be a good solution too with a cli option to set it (like we have for app or lib already). Either way, opt in rather than out.

command-tab commented 3 weeks ago

A couple of Python tools come to mind that do load .env files by default. For example, pipenv run ... and flask run do this out of the box in opt-out mode. pipenv considers the PIPENV_DONT_LOAD_ENV env var, and Flask uses FLASK_SKIP_DOTENV. As someone who uses pipenv for day-to-day application development, I am excited for uv to gain auto-loading of .env files by default. Having to add a pyproject.toml entry like read_dotenv_files = true would be OK, though I'm sure I would forget to add it occasionally in new projects.

seapagan commented 3 weeks ago

For example, pipenv run ... and flask run do this out of the box in opt-out mode.

'Flask run' is however less of a tool in the 'uv' mold than very specific to Flask users, not going to be used by EVERY Python project and environment you write. It's more equivalent to 'django-admin'.

I'm not saying this is a bad thing, just make it opt-in so there are no surprises. The large majority of users generally don't read release notes🤷‍♂️

bluss commented 3 weeks ago

One facet of it is that uv run is very versatile, and not just used in uv projects, but could be used in any directory, and with/without existing project or virtual environment. And you can also use uv run to run things in a different directory like uv run --python ./path/to/venv or uv run --project foo which also puts some distance between the .env file and the installed python software. Anchoring env file support to a configuration or a uv project would be a more consistent model, possibly(?)

TheRealBecks commented 3 weeks ago

After the migration from pipenv to uv our local Ansible environments are broken as uv does not read the .env file by default.

Also Docker ~and Ansible~ read .env by default. Edit: My bad, Ansible is not loading the environment via .env, that's exactly the use case why I need uv to load it for Ansible on my local dev environment! 😀

In my case (what our company does): The .env files are only needed for local development where users also run uv run ... manually. Opt-in would be no comfort from the users perspective especially when projects will be migrated from tools like pipenv to uv. Also production systems get different environment variables that will be set for the run and will not use any .env file and if I would like to change that behaviour I will use the UV_xx environment variables.

So, maybe this approach will be accepted: 1) Use opt-out by default, so uv reads the .env file by default 2) Add UV_xx environment variables for not reading and reading .env files: You already planned this feature 3) Add another option for the pyproject.toml file so reading an .env file for a project can be set to opt-in instead of the default opt-out

@seapagan @AndreuCodina Would option 3 something you would like to see in uv?

seapagan commented 3 weeks ago

@TheRealBecks

My preferred choice would still be opt-in by default but I can see that's unlikely to happen 😂

I've never really used 'pipenv' (went pip to poetry to uv) so the whole concept of auto-loading env files is strange to me.

Yeah, option 3 would work but I'd really love a global setting in the configuration file too, so I can opt-out for every project and just forget about it. I've not sat down and gone through the PR in depth yet so apologies if this is already there.

jankatins commented 3 weeks ago

I don't really care as long as there is an env var which can overwrite the behaviour. I let direnv load .envs and so would disable it for uv.

We also have some projects where the .env is in a parent folder, not the repo folder. So I guess for these repos, it wouldn't work anyways.

joshsleeper commented 3 weeks ago

Opt-in would be no comfort from the users perspective especially when projects will be migrated from tools like pipenv to uv.

@TheRealBecks fwiw this doesn't track with me as an argument for opt-in by default.

If you're already having to do various other config changes and generate new lock files (you almost certainly are) and anything else in the migration process I struggle to see how marginally tweaking the new tool's config isn't entirely expected anyway. Migrating between these tools isn't overly hard, but it's never as simple as literally just calling a different tool with 0 config changes either.

gusutabopb commented 3 weeks ago

Isn't placing a file in the directory you are running uv from named exactly .env a sign you're opting-in already? If you have a .env-formatted file that you don't want auto-loaded by uv, then just name it something else.

Asking uv to force users to pass an "opt-in" flag every time (or set it via uv.toml) to load .env files is like saying git should require that users pass a --use-ignore-file (or equivalent in .git/config) to use .gitignore, or asking bash to require users to pass some flag to read .bashrc, etc.

I understand there are cases where opting out or explicitly passing a specific file would be needed (e.g. multiple dotenv variants (.env.stage, .env.dev, etc), dotenv files in other directories, etc) but having uv auto-load a plain .env file in the cwd is an absolutely reasonable default IMHO.

AndreuCodina commented 3 weeks ago

I can give a practical example:

.env and .env.myenvironment is the Python convention to store settings used by the code, and the code should decide if it gives priority to .env files or other sources. The settings must be managed by code, and the execution of the code the simplest and the same in all environments: uv run.

myenvironment is a value obtained with an environment variable, so:

dotenv = DotEnvSettingsSource(
    settings_cls,
    env_file=[".env", f".env.{os.environ["PYTHON_ENVIRONMENT"]"],
    env_nested_delimiter="__",
    case_sensitive=True,
)

Then, even if you're using pydantic-settings or a runner tool (uv), you'll need an environment variable.

So, we do agree, we need to provide an environment variable, but the issue is how.

With code is simple, but verbose (because we need this file in every project):

class ApplicationEnvironment(StrEnum):
    LOCAL = "Local"
    DEVELOPMENT = "Development"
    STAGING = "Staging"
    PRODUCTION = "Production"

    @staticmethod
    def get_current() -> str:
        return os.getenv("PYTHON_ENVIRONMENT", ApplicationEnvironment.LOCAL)

and it'll be used this way:

dotenv = DotEnvSettingsSource(env_file=[".env", f".env.{ApplicationEnvironment.get_current()}"],

This environment variable will be used for configuring the code itself, e.g. not adding telemetry if you're in localhost, adding secrets from Azure Key Vault if you're not in localhost, not showing the Swagger documentation in production, etc.

docs_url = (
    "/docs"
    if ApplicationEnvironment.get_current() != ApplicationEnvironment.PRODUCTION
    else None
)
app = FastAPI(
    docs_url=docs_url,
    redoc_url=None,
)

So, the thing is, when we deploy an application, we provide an environment variable on the cloud or with docker run, and the proper .env.myenvironment file is read.

But in localhost we can't do that, so where do we specify the environment variable?

.env files are for code configuration, so use another thing: pyproject.toml, mynewfile.json, etc.

In .NET we have a command for everything called dotnet, and it's the uv equivalent.

The launchSettings.json file is used in ASP.NET Core to configure how an application is launched during development, and you can specify environment variables and tool dotnet configurations.

In summary, uv or your framework should provide a (configurable) default environment variable to run the project, but not from the code settings. And this environment variable will be different in each environment, not relying on different uv parameters to read from here of there.

What environment variables do you need? TBH I don't think more than the PYTHON_ENVIRONMENT variable is necessary. On the cloud, with user-assigned managed identities, you add for example the AZURE_CLIENT_ID environment variable, but when you run the code in localhost, you don't set it by hand, you execute az login.

charliermarsh commented 3 weeks ago

(Candidly, I don't find this framing very convincing because dotenv loading is something that other runners support and is much-used and appreciated by users of those tools.)