Disallow vdeps in packages.yaml
(#2699)
* Consolidate packages.yaml code to preferred_packages * Add validation check and a test for packages.py parsing. * flake8
This commit is contained in:
parent
040f8a7176
commit
b9ec69dce1
@ -77,7 +77,7 @@
|
|||||||
import spack.config
|
import spack.config
|
||||||
import spack.fetch_strategy
|
import spack.fetch_strategy
|
||||||
from spack.file_cache import FileCache
|
from spack.file_cache import FileCache
|
||||||
from spack.preferred_packages import PreferredPackages
|
from spack.package_prefs import PreferredPackages
|
||||||
from spack.abi import ABI
|
from spack.abi import ABI
|
||||||
from spack.concretize import DefaultConcretizer
|
from spack.concretize import DefaultConcretizer
|
||||||
from spack.version import Version
|
from spack.version import Version
|
||||||
@ -99,11 +99,6 @@
|
|||||||
tty.die('while initializing Spack RepoPath:', e.message)
|
tty.die('while initializing Spack RepoPath:', e.message)
|
||||||
|
|
||||||
|
|
||||||
# PreferredPackages controls preference sort order during concretization.
|
|
||||||
# More preferred packages are sorted first.
|
|
||||||
pkgsort = PreferredPackages()
|
|
||||||
|
|
||||||
|
|
||||||
# Tests ABI compatibility between packages
|
# Tests ABI compatibility between packages
|
||||||
abi = ABI()
|
abi = ABI()
|
||||||
|
|
||||||
|
@ -42,8 +42,7 @@
|
|||||||
from spack.version import *
|
from spack.version import *
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from spack.config import *
|
from spack.package_prefs import *
|
||||||
import spack.preferred_packages
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultConcretizer(object):
|
class DefaultConcretizer(object):
|
||||||
@ -64,11 +63,11 @@ def _valid_virtuals_and_externals(self, spec):
|
|||||||
raise UnsatisfiableProviderSpecError(providers[0], spec)
|
raise UnsatisfiableProviderSpecError(providers[0], spec)
|
||||||
spec_w_preferred_providers = find_spec(
|
spec_w_preferred_providers = find_spec(
|
||||||
spec,
|
spec,
|
||||||
lambda x: spack.pkgsort.spec_has_preferred_provider(
|
lambda x: pkgsort().spec_has_preferred_provider(
|
||||||
x.name, spec.name))
|
x.name, spec.name))
|
||||||
if not spec_w_preferred_providers:
|
if not spec_w_preferred_providers:
|
||||||
spec_w_preferred_providers = spec
|
spec_w_preferred_providers = spec
|
||||||
provider_cmp = partial(spack.pkgsort.provider_compare,
|
provider_cmp = partial(pkgsort().provider_compare,
|
||||||
spec_w_preferred_providers.name,
|
spec_w_preferred_providers.name,
|
||||||
spec.name)
|
spec.name)
|
||||||
candidates = sorted(providers, cmp=provider_cmp)
|
candidates = sorted(providers, cmp=provider_cmp)
|
||||||
@ -169,8 +168,8 @@ def concretize_version(self, spec):
|
|||||||
|
|
||||||
# ---------- Produce prioritized list of versions
|
# ---------- Produce prioritized list of versions
|
||||||
# Get list of preferences from packages.yaml
|
# Get list of preferences from packages.yaml
|
||||||
preferred = spack.pkgsort
|
preferred = pkgsort()
|
||||||
# NOTE: spack.pkgsort == spack.preferred_packages.PreferredPackages()
|
# NOTE: pkgsort() == spack.package_prefs.PreferredPackages()
|
||||||
|
|
||||||
yaml_specs = [
|
yaml_specs = [
|
||||||
x[0] for x in
|
x[0] for x in
|
||||||
@ -277,7 +276,7 @@ def concretize_variants(self, spec):
|
|||||||
the package specification.
|
the package specification.
|
||||||
"""
|
"""
|
||||||
changed = False
|
changed = False
|
||||||
preferred_variants = spack.pkgsort.spec_preferred_variants(
|
preferred_variants = pkgsort().spec_preferred_variants(
|
||||||
spec.package_class.name)
|
spec.package_class.name)
|
||||||
for name, variant in spec.package_class.variants.items():
|
for name, variant in spec.package_class.variants.items():
|
||||||
if name not in spec.variants:
|
if name not in spec.variants:
|
||||||
@ -342,7 +341,7 @@ def _proper_compiler_style(cspec, aspec):
|
|||||||
compiler_list = all_compilers if not other_compiler else \
|
compiler_list = all_compilers if not other_compiler else \
|
||||||
spack.compilers.find(other_compiler)
|
spack.compilers.find(other_compiler)
|
||||||
cmp_compilers = partial(
|
cmp_compilers = partial(
|
||||||
spack.pkgsort.compiler_compare, other_spec.name)
|
pkgsort().compiler_compare, other_spec.name)
|
||||||
matches = sorted(compiler_list, cmp=cmp_compilers)
|
matches = sorted(compiler_list, cmp=cmp_compilers)
|
||||||
if not matches:
|
if not matches:
|
||||||
arch = spec.architecture
|
arch = spec.architecture
|
||||||
@ -467,41 +466,6 @@ def find_spec(spec, condition):
|
|||||||
return None # Nothing matched the condition.
|
return None # Nothing matched the condition.
|
||||||
|
|
||||||
|
|
||||||
def cmp_specs(lhs, rhs):
|
|
||||||
# Package name sort order is not configurable, always goes alphabetical
|
|
||||||
if lhs.name != rhs.name:
|
|
||||||
return cmp(lhs.name, rhs.name)
|
|
||||||
|
|
||||||
# Package version is second in compare order
|
|
||||||
pkgname = lhs.name
|
|
||||||
if lhs.versions != rhs.versions:
|
|
||||||
return spack.pkgsort.version_compare(
|
|
||||||
pkgname, lhs.versions, rhs.versions)
|
|
||||||
|
|
||||||
# Compiler is third
|
|
||||||
if lhs.compiler != rhs.compiler:
|
|
||||||
return spack.pkgsort.compiler_compare(
|
|
||||||
pkgname, lhs.compiler, rhs.compiler)
|
|
||||||
|
|
||||||
# Variants
|
|
||||||
if lhs.variants != rhs.variants:
|
|
||||||
return spack.pkgsort.variant_compare(
|
|
||||||
pkgname, lhs.variants, rhs.variants)
|
|
||||||
|
|
||||||
# Architecture
|
|
||||||
if lhs.architecture != rhs.architecture:
|
|
||||||
return spack.pkgsort.architecture_compare(
|
|
||||||
pkgname, lhs.architecture, rhs.architecture)
|
|
||||||
|
|
||||||
# Dependency is not configurable
|
|
||||||
lhash, rhash = hash(lhs), hash(rhs)
|
|
||||||
if lhash != rhash:
|
|
||||||
return -1 if lhash < rhash else 1
|
|
||||||
|
|
||||||
# Equal specs
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class UnavailableCompilerVersionError(spack.error.SpackError):
|
class UnavailableCompilerVersionError(spack.error.SpackError):
|
||||||
|
|
||||||
"""Raised when there is no available compiler that satisfies a
|
"""Raised when there is no available compiler that satisfies a
|
||||||
|
@ -199,6 +199,7 @@ def clear(self):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<ConfigScope: %s: %s>' % (self.name, self.path)
|
return '<ConfigScope: %s: %s>' % (self.name, self.path)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Below are configuration scopes.
|
# Below are configuration scopes.
|
||||||
#
|
#
|
||||||
@ -458,54 +459,6 @@ def print_section(section):
|
|||||||
raise ConfigError("Error reading configuration: %s" % section)
|
raise ConfigError("Error reading configuration: %s" % section)
|
||||||
|
|
||||||
|
|
||||||
def spec_externals(spec):
|
|
||||||
"""Return a list of external specs (with external directory path filled in),
|
|
||||||
one for each known external installation."""
|
|
||||||
# break circular import.
|
|
||||||
from spack.build_environment import get_path_from_module
|
|
||||||
|
|
||||||
allpkgs = get_config('packages')
|
|
||||||
name = spec.name
|
|
||||||
|
|
||||||
external_specs = []
|
|
||||||
pkg_paths = allpkgs.get(name, {}).get('paths', None)
|
|
||||||
pkg_modules = allpkgs.get(name, {}).get('modules', None)
|
|
||||||
if (not pkg_paths) and (not pkg_modules):
|
|
||||||
return []
|
|
||||||
|
|
||||||
for external_spec, path in pkg_paths.iteritems():
|
|
||||||
if not path:
|
|
||||||
# skip entries without paths (avoid creating extra Specs)
|
|
||||||
continue
|
|
||||||
|
|
||||||
external_spec = spack.spec.Spec(external_spec, external=path)
|
|
||||||
if external_spec.satisfies(spec):
|
|
||||||
external_specs.append(external_spec)
|
|
||||||
|
|
||||||
for external_spec, module in pkg_modules.iteritems():
|
|
||||||
if not module:
|
|
||||||
continue
|
|
||||||
|
|
||||||
path = get_path_from_module(module)
|
|
||||||
|
|
||||||
external_spec = spack.spec.Spec(
|
|
||||||
external_spec, external=path, external_module=module)
|
|
||||||
if external_spec.satisfies(spec):
|
|
||||||
external_specs.append(external_spec)
|
|
||||||
|
|
||||||
return external_specs
|
|
||||||
|
|
||||||
|
|
||||||
def is_spec_buildable(spec):
|
|
||||||
"""Return true if the spec pkgspec is configured as buildable"""
|
|
||||||
allpkgs = get_config('packages')
|
|
||||||
if spec.name not in allpkgs:
|
|
||||||
return True
|
|
||||||
if 'buildable' not in allpkgs[spec.name]:
|
|
||||||
return True
|
|
||||||
return allpkgs[spec.name]['buildable']
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(SpackError):
|
class ConfigError(SpackError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -24,12 +24,33 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
import spack
|
import spack
|
||||||
|
import spack.error
|
||||||
from spack.version import *
|
from spack.version import *
|
||||||
|
|
||||||
|
|
||||||
|
def get_packages_config():
|
||||||
|
"""Wrapper around get_packages_config() to validate semantics."""
|
||||||
|
config = spack.config.get_config('packages')
|
||||||
|
|
||||||
|
# Get a list of virtuals from packages.yaml. Note that because we
|
||||||
|
# check spack.repo, this collects virtuals that are actually provided
|
||||||
|
# by sometihng, not just packages/names that don't exist.
|
||||||
|
# So, this won't include, e.g., 'all'.
|
||||||
|
virtuals = [(pkg_name, pkg_name._start_mark) for pkg_name in config
|
||||||
|
if spack.repo.is_virtual(pkg_name)]
|
||||||
|
|
||||||
|
# die if there are virtuals in `packages.py`
|
||||||
|
if virtuals:
|
||||||
|
errors = ["%s: %s" % (line_info, name) for name, line_info in virtuals]
|
||||||
|
raise VirtualInPackagesYAMLError(
|
||||||
|
"packages.yaml entries cannot be virtual packages:", *errors)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
class PreferredPackages(object):
|
class PreferredPackages(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.preferred = spack.config.get_config('packages')
|
self.preferred = get_packages_config()
|
||||||
self._spec_for_pkgname_cache = {}
|
self._spec_for_pkgname_cache = {}
|
||||||
|
|
||||||
# Given a package name, sort component (e.g, version, compiler, ...), and
|
# Given a package name, sort component (e.g, version, compiler, ...), and
|
||||||
@ -194,3 +215,100 @@ def compiler_compare(self, pkgname, a, b):
|
|||||||
pkgname. One compiler is less-than another if it is preferred over
|
pkgname. One compiler is less-than another if it is preferred over
|
||||||
the other."""
|
the other."""
|
||||||
return self._spec_compare(pkgname, 'compiler', a, b, False, None)
|
return self._spec_compare(pkgname, 'compiler', a, b, False, None)
|
||||||
|
|
||||||
|
|
||||||
|
def spec_externals(spec):
|
||||||
|
"""Return a list of external specs (with external directory path filled in),
|
||||||
|
one for each known external installation."""
|
||||||
|
# break circular import.
|
||||||
|
from spack.build_environment import get_path_from_module
|
||||||
|
|
||||||
|
allpkgs = get_packages_config()
|
||||||
|
name = spec.name
|
||||||
|
|
||||||
|
external_specs = []
|
||||||
|
pkg_paths = allpkgs.get(name, {}).get('paths', None)
|
||||||
|
pkg_modules = allpkgs.get(name, {}).get('modules', None)
|
||||||
|
if (not pkg_paths) and (not pkg_modules):
|
||||||
|
return []
|
||||||
|
|
||||||
|
for external_spec, path in pkg_paths.iteritems():
|
||||||
|
if not path:
|
||||||
|
# skip entries without paths (avoid creating extra Specs)
|
||||||
|
continue
|
||||||
|
|
||||||
|
external_spec = spack.spec.Spec(external_spec, external=path)
|
||||||
|
if external_spec.satisfies(spec):
|
||||||
|
external_specs.append(external_spec)
|
||||||
|
|
||||||
|
for external_spec, module in pkg_modules.iteritems():
|
||||||
|
if not module:
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = get_path_from_module(module)
|
||||||
|
|
||||||
|
external_spec = spack.spec.Spec(
|
||||||
|
external_spec, external=path, external_module=module)
|
||||||
|
if external_spec.satisfies(spec):
|
||||||
|
external_specs.append(external_spec)
|
||||||
|
|
||||||
|
return external_specs
|
||||||
|
|
||||||
|
|
||||||
|
def is_spec_buildable(spec):
|
||||||
|
"""Return true if the spec pkgspec is configured as buildable"""
|
||||||
|
allpkgs = get_packages_config()
|
||||||
|
if spec.name not in allpkgs:
|
||||||
|
return True
|
||||||
|
if 'buildable' not in allpkgs[spec.name]:
|
||||||
|
return True
|
||||||
|
return allpkgs[spec.name]['buildable']
|
||||||
|
|
||||||
|
|
||||||
|
def cmp_specs(lhs, rhs):
|
||||||
|
# Package name sort order is not configurable, always goes alphabetical
|
||||||
|
if lhs.name != rhs.name:
|
||||||
|
return cmp(lhs.name, rhs.name)
|
||||||
|
|
||||||
|
# Package version is second in compare order
|
||||||
|
pkgname = lhs.name
|
||||||
|
if lhs.versions != rhs.versions:
|
||||||
|
return pkgsort().version_compare(
|
||||||
|
pkgname, lhs.versions, rhs.versions)
|
||||||
|
|
||||||
|
# Compiler is third
|
||||||
|
if lhs.compiler != rhs.compiler:
|
||||||
|
return pkgsort().compiler_compare(
|
||||||
|
pkgname, lhs.compiler, rhs.compiler)
|
||||||
|
|
||||||
|
# Variants
|
||||||
|
if lhs.variants != rhs.variants:
|
||||||
|
return pkgsort().variant_compare(
|
||||||
|
pkgname, lhs.variants, rhs.variants)
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
if lhs.architecture != rhs.architecture:
|
||||||
|
return pkgsort().architecture_compare(
|
||||||
|
pkgname, lhs.architecture, rhs.architecture)
|
||||||
|
|
||||||
|
# Dependency is not configurable
|
||||||
|
lhash, rhash = hash(lhs), hash(rhs)
|
||||||
|
if lhash != rhash:
|
||||||
|
return -1 if lhash < rhash else 1
|
||||||
|
|
||||||
|
# Equal specs
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
_pkgsort = None
|
||||||
|
|
||||||
|
|
||||||
|
def pkgsort():
|
||||||
|
global _pkgsort
|
||||||
|
if _pkgsort is None:
|
||||||
|
_pkgsort = PreferredPackages()
|
||||||
|
return _pkgsort
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualInPackagesYAMLError(spack.error.SpackError):
|
||||||
|
"""Raised when a disallowed virtual is found in packages.yaml"""
|
@ -337,8 +337,16 @@ def filename_for_package_name(self, pkg_name):
|
|||||||
return self.repo_for_pkg(pkg_name).filename_for_package_name(pkg_name)
|
return self.repo_for_pkg(pkg_name).filename_for_package_name(pkg_name)
|
||||||
|
|
||||||
def exists(self, pkg_name):
|
def exists(self, pkg_name):
|
||||||
|
"""Whether package with the give name exists in the path's repos.
|
||||||
|
|
||||||
|
Note that virtual packages do not "exist".
|
||||||
|
"""
|
||||||
return any(repo.exists(pkg_name) for repo in self.repos)
|
return any(repo.exists(pkg_name) for repo in self.repos)
|
||||||
|
|
||||||
|
def is_virtual(self, pkg_name):
|
||||||
|
"""True if the package with this name is virtual, False otherwise."""
|
||||||
|
return pkg_name in self.provider_index
|
||||||
|
|
||||||
def __contains__(self, pkg_name):
|
def __contains__(self, pkg_name):
|
||||||
return self.exists(pkg_name)
|
return self.exists(pkg_name)
|
||||||
|
|
||||||
@ -772,6 +780,10 @@ def exists(self, pkg_name):
|
|||||||
filename = self.filename_for_package_name(pkg_name)
|
filename = self.filename_for_package_name(pkg_name)
|
||||||
return os.path.exists(filename)
|
return os.path.exists(filename)
|
||||||
|
|
||||||
|
def is_virtual(self, pkg_name):
|
||||||
|
"""True if the package with this name is virtual, False otherwise."""
|
||||||
|
return self.provider_index.contains(pkg_name)
|
||||||
|
|
||||||
def _get_pkg_module(self, pkg_name):
|
def _get_pkg_module(self, pkg_name):
|
||||||
"""Create a module for a particular package.
|
"""Create a module for a particular package.
|
||||||
|
|
||||||
|
@ -2546,6 +2546,8 @@ def dep_string(self):
|
|||||||
return ''.join("^" + dep.format() for dep in self.sorted_deps())
|
return ''.join("^" + dep.format() for dep in self.sorted_deps())
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
|
from package_prefs import pkgsort
|
||||||
|
|
||||||
# Package name sort order is not configurable, always goes alphabetical
|
# Package name sort order is not configurable, always goes alphabetical
|
||||||
if self.name != other.name:
|
if self.name != other.name:
|
||||||
return cmp(self.name, other.name)
|
return cmp(self.name, other.name)
|
||||||
@ -2553,22 +2555,22 @@ def __cmp__(self, other):
|
|||||||
# Package version is second in compare order
|
# Package version is second in compare order
|
||||||
pkgname = self.name
|
pkgname = self.name
|
||||||
if self.versions != other.versions:
|
if self.versions != other.versions:
|
||||||
return spack.pkgsort.version_compare(
|
return pkgsort().version_compare(
|
||||||
pkgname, self.versions, other.versions)
|
pkgname, self.versions, other.versions)
|
||||||
|
|
||||||
# Compiler is third
|
# Compiler is third
|
||||||
if self.compiler != other.compiler:
|
if self.compiler != other.compiler:
|
||||||
return spack.pkgsort.compiler_compare(
|
return pkgsort().compiler_compare(
|
||||||
pkgname, self.compiler, other.compiler)
|
pkgname, self.compiler, other.compiler)
|
||||||
|
|
||||||
# Variants
|
# Variants
|
||||||
if self.variants != other.variants:
|
if self.variants != other.variants:
|
||||||
return spack.pkgsort.variant_compare(
|
return pkgsort().variant_compare(
|
||||||
pkgname, self.variants, other.variants)
|
pkgname, self.variants, other.variants)
|
||||||
|
|
||||||
# Target
|
# Target
|
||||||
if self.architecture != other.architecture:
|
if self.architecture != other.architecture:
|
||||||
return spack.pkgsort.architecture_compare(
|
return pkgsort().architecture_compare(
|
||||||
pkgname, self.architecture, other.architecture)
|
pkgname, self.architecture, other.architecture)
|
||||||
|
|
||||||
# Dependency is not configurable
|
# Dependency is not configurable
|
||||||
|
@ -25,7 +25,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import spack
|
import spack
|
||||||
|
import spack.util.spack_yaml as syaml
|
||||||
from spack.spec import Spec
|
from spack.spec import Spec
|
||||||
|
from spack.package_prefs import PreferredPackages
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@ -39,7 +41,7 @@ def concretize_scope(config, tmpdir):
|
|||||||
# This is kind of weird, but that's how config scopes are
|
# This is kind of weird, but that's how config scopes are
|
||||||
# set in ConfigScope.__init__
|
# set in ConfigScope.__init__
|
||||||
spack.config.config_scopes.pop('concretize')
|
spack.config.config_scopes.pop('concretize')
|
||||||
spack.pkgsort = spack.PreferredPackages()
|
spack.package_prefs._pkgsort = PreferredPackages()
|
||||||
|
|
||||||
|
|
||||||
def concretize(abstract_spec):
|
def concretize(abstract_spec):
|
||||||
@ -50,7 +52,7 @@ def update_packages(pkgname, section, value):
|
|||||||
"""Update config and reread package list"""
|
"""Update config and reread package list"""
|
||||||
conf = {pkgname: {section: value}}
|
conf = {pkgname: {section: value}}
|
||||||
spack.config.update_config('packages', conf, 'concretize')
|
spack.config.update_config('packages', conf, 'concretize')
|
||||||
spack.pkgsort = spack.PreferredPackages()
|
spack.package_prefs._pkgsort = PreferredPackages()
|
||||||
|
|
||||||
|
|
||||||
def assert_variant_values(spec, **variants):
|
def assert_variant_values(spec, **variants):
|
||||||
@ -114,3 +116,30 @@ def test_develop(self):
|
|||||||
spec = Spec('builtin.mock.develop-test')
|
spec = Spec('builtin.mock.develop-test')
|
||||||
spec.concretize()
|
spec.concretize()
|
||||||
assert spec.version == spack.spec.Version('0.2.15')
|
assert spec.version == spack.spec.Version('0.2.15')
|
||||||
|
|
||||||
|
def test_no_virtuals_in_packages_yaml(self):
|
||||||
|
"""Verify that virtuals are not allowed in packages.yaml."""
|
||||||
|
|
||||||
|
# set up a packages.yaml file with a vdep as a key. We use
|
||||||
|
# syaml.load here to make sure source lines in the config are
|
||||||
|
# attached to parsed strings, as the error message uses them.
|
||||||
|
conf = syaml.load("""mpi:
|
||||||
|
paths:
|
||||||
|
mpi-with-lapack@2.1: /path/to/lapack
|
||||||
|
""")
|
||||||
|
spack.config.update_config('packages', conf, 'concretize')
|
||||||
|
|
||||||
|
# now when we get the packages.yaml config, there should be an error
|
||||||
|
with pytest.raises(spack.package_prefs.VirtualInPackagesYAMLError):
|
||||||
|
spack.package_prefs.get_packages_config()
|
||||||
|
|
||||||
|
def test_all_is_not_a_virtual(self):
|
||||||
|
"""Verify that `all` is allowed in packages.yaml."""
|
||||||
|
conf = syaml.load("""all:
|
||||||
|
variants: [+mpi]
|
||||||
|
""")
|
||||||
|
spack.config.update_config('packages', conf, 'concretize')
|
||||||
|
|
||||||
|
# should be no error for 'all':
|
||||||
|
spack.package_prefs._pkgsort = PreferredPackages()
|
||||||
|
spack.package_prefs.get_packages_config()
|
||||||
|
Loading…
Reference in New Issue
Block a user