Repository: dask/dask-jobqueue

Issue:
PBSCluster hard codes account string for project
Currently `PBSCluster` accepts a project code as an argument and uses this with the account string, `-A`, option

https://github.com/dask/dask-jobqueue/blob/master/dask_jobqueue/pbs.py#L96

PBS Pro has account string (`-A`) and project (`-P`) options:

https://github.com/PBSPro/pbspro/blob/master/src/cmds/qsub.c#L892-L898

For my local HPC setup the `-P` option is used for project based accounting. 

It would be preferable to have the ability to pass either `account` or `project` as options for `PBSCluster` and use the appropriate flag. This would break existing configs that assume the `-A` flag is used.

One possible solutions would be to allow the flags associated with options to be specified in a `dask` config file.

Would the maintainers be happy to accept a PR with this implemented?

Discussion:

Patch:
diff --git a/dask_jobqueue/core.py b/dask_jobqueue/core.py
index ff7ecd1..0ed3dc5 100644
--- a/dask_jobqueue/core.py
+++ b/dask_jobqueue/core.py
@@ -285,7 +285,7 @@ class Job(ProcessInterface, abc.ABC):
         if header_skip is not None:
             warn = (
                 "header_skip has been renamed to job_directives_skip. "
-                "You are still using it (even if only set to (); please also check config files). "
+                "You are still using it (even if only set to []; please also check config files). "
                 "If you did not set job_directives_skip yet, header_skip will be respected for now, "
                 "but it will be removed in a future release. "
                 "If you already set job_directives_skip, header_skip is ignored and you can remove it."
diff --git a/dask_jobqueue/jobqueue.yaml b/dask_jobqueue/jobqueue.yaml
index a9f57b5..6432c28 100644
--- a/dask_jobqueue/jobqueue.yaml
+++ b/dask_jobqueue/jobqueue.yaml
@@ -48,7 +48,7 @@ jobqueue:
     # PBS resource manager options
     shebang: "#!/usr/bin/env bash"
     queue: null
-    project: null
+    account: null
     walltime: '00:30:00'
     env-extra: null
     job-script-prologue: []
@@ -110,7 +110,7 @@ jobqueue:
     # SLURM resource manager options
     shebang: "#!/usr/bin/env bash"
     queue: null
-    project: null
+    account: null
     walltime: '00:30:00'
     env-extra: null
     job-script-prologue: []
@@ -142,7 +142,7 @@ jobqueue:
     # PBS resource manager options
     shebang: "#!/usr/bin/env bash"
     queue: null
-    project: null
+    account: null
     walltime: '00:30:00'
     env-extra: null
     job-script-prologue: []
diff --git a/dask_jobqueue/lsf.py b/dask_jobqueue/lsf.py
index b469a3b..d165371 100644
--- a/dask_jobqueue/lsf.py
+++ b/dask_jobqueue/lsf.py
@@ -185,7 +185,7 @@ class LSFCluster(JobQueueCluster):
     queue : str
         Destination queue for each worker job. Passed to `#BSUB -q` option.
     project : str
-        Accounting string associated with each worker job. Passed to
+        Project associated with each worker job. Passed to
         `#BSUB -P` option.
     {job}
     ncpus : int
diff --git a/dask_jobqueue/oar.py b/dask_jobqueue/oar.py
index 9d69385..cce0e22 100644
--- a/dask_jobqueue/oar.py
+++ b/dask_jobqueue/oar.py
@@ -112,7 +112,7 @@ class OARCluster(JobQueueCluster):
     queue : str
         Destination queue for each worker job. Passed to `#OAR -q` option.
     project : str
-        Accounting string associated with each worker job. Passed to `#OAR -p` option.
+        Project associated with each worker job. Passed to `#OAR --project` option.
     {job}
     {cluster}
     resource_spec : str
diff --git a/dask_jobqueue/pbs.py b/dask_jobqueue/pbs.py
index e3569c2..719fd9c 100644
--- a/dask_jobqueue/pbs.py
+++ b/dask_jobqueue/pbs.py
@@ -1,6 +1,7 @@
 import logging
 import math
 import os
+import warnings
 
 import dask
 
@@ -45,6 +46,7 @@ class PBSJob(Job):
         name=None,
         queue=None,
         project=None,
+        account=None,
         resource_spec=None,
         walltime=None,
         config_name=None,
@@ -63,13 +65,23 @@ class PBSJob(Job):
         if walltime is None:
             walltime = dask.config.get("jobqueue.%s.walltime" % self.config_name)
 
-        if project is None:
-            project = dask.config.get(
-                "jobqueue.%s.project" % self.config_name
+        if account is None:
+            account = dask.config.get(
+                "jobqueue.%s.account" % self.config_name
             ) or os.environ.get("PBS_ACCOUNT")
-
-        # Try to find a project name from environment variable
-        project = project or os.environ.get("PBS_ACCOUNT")
+        if project is None:
+            project = dask.config.get("jobqueue.%s.project" % self.config_name, None)
+        if project is not None:
+            warn = (
+                "project has been renamed to account as this kwarg was used wit -A option. "
+                "You are still using it (please also check config files). "
+                "If you did not set account yet, project will be respected for now, "
+                "but it will be removed in a future release. "
+                "If you already set account, project is ignored and you can remove it."
+            )
+            warnings.warn(warn, FutureWarning)
+            if not account:
+                account = project
 
         header_lines = []
         # PBS header build
@@ -77,8 +89,8 @@ class PBSJob(Job):
             header_lines.append("#PBS -N %s" % self.job_name)
         if queue is not None:
             header_lines.append("#PBS -q %s" % queue)
-        if project is not None:
-            header_lines.append("#PBS -A %s" % project)
+        if account is not None:
+            header_lines.append("#PBS -A %s" % account)
         if resource_spec is None:
             # Compute default resources specifications
             resource_spec = "select=1:ncpus=%d" % self.worker_cores
@@ -121,6 +133,8 @@ class PBSCluster(JobQueueCluster):
     queue : str
         Destination queue for each worker job. Passed to `#PBS -q` option.
     project : str
+        Deprecated: use ``account`` instead. This parameter will be removed in a future version.
+    account : str
         Accounting string associated with each worker job. Passed to `#PBS -A` option.
     {job}
     {cluster}
@@ -136,7 +150,7 @@ class PBSCluster(JobQueueCluster):
     Examples
     --------
     >>> from dask_jobqueue import PBSCluster
-    >>> cluster = PBSCluster(queue='regular', project="myproj", cores=24,
+    >>> cluster = PBSCluster(queue='regular', account="myaccountingstring", cores=24,
     ...     memory="500 GB")
     >>> cluster.scale(jobs=10)  # ask for 10 jobs
 
diff --git a/dask_jobqueue/sge.py b/dask_jobqueue/sge.py
index 26dfccc..64bb1b4 100644
--- a/dask_jobqueue/sge.py
+++ b/dask_jobqueue/sge.py
@@ -84,7 +84,7 @@ class SGECluster(JobQueueCluster):
     queue : str
         Destination queue for each worker job. Passed to `#$ -q` option.
     project : str
-        Accounting string associated with each worker job. Passed to `#$ -A` option.
+        Project associated with each worker job. Passed to `#$ -P` option.
     {job}
     {cluster}
     resource_spec : str
diff --git a/dask_jobqueue/slurm.py b/dask_jobqueue/slurm.py
index 571b8d6..de306df 100644
--- a/dask_jobqueue/slurm.py
+++ b/dask_jobqueue/slurm.py
@@ -1,5 +1,6 @@
 import logging
 import math
+import warnings
 
 import dask
 
@@ -20,6 +21,7 @@ class SLURMJob(Job):
         name=None,
         queue=None,
         project=None,
+        account=None,
         walltime=None,
         job_cpu=None,
         job_mem=None,
@@ -32,8 +34,22 @@ class SLURMJob(Job):
 
         if queue is None:
             queue = dask.config.get("jobqueue.%s.queue" % self.config_name)
+        if account is None:
+            account = dask.config.get("jobqueue.%s.account" % self.config_name)
         if project is None:
-            project = dask.config.get("jobqueue.%s.project" % self.config_name)
+            project = dask.config.get("jobqueue.%s.project" % self.config_name, None)
+        if project is not None:
+            warn = (
+                "project has been renamed to account as this kwarg was used wit -A option. "
+                "You are still using it (please also check config files). "
+                "If you did not set account yet, project will be respected for now, "
+                "but it will be removed in a future release. "
+                "If you already set account, project is ignored and you can remove it."
+            )
+            warnings.warn(warn, FutureWarning)
+            if not account:
+                account = project
+
         if walltime is None:
             walltime = dask.config.get("jobqueue.%s.walltime" % self.config_name)
         if job_cpu is None:
@@ -56,8 +72,8 @@ class SLURMJob(Job):
             )
         if queue is not None:
             header_lines.append("#SBATCH -p %s" % queue)
-        if project is not None:
-            header_lines.append("#SBATCH -A %s" % project)
+        if account is not None:
+            header_lines.append("#SBATCH -A %s" % account)
 
         # Init resources, always 1 task,
         # and then number of cpu is processes * threads if not set
@@ -123,7 +139,9 @@ class SLURMCluster(JobQueueCluster):
     queue : str
         Destination queue for each worker job. Passed to `#SBATCH -p` option.
     project : str
-        Accounting string associated with each worker job. Passed to `#SBATCH -A` option.
+        Deprecated: use ``account`` instead. This parameter will be removed in a future version.
+    account : str
+        Accounting string associated with each worker job. Passed to `#PBS -A` option.
     {job}
     {cluster}
     walltime : str
@@ -143,7 +161,7 @@ class SLURMCluster(JobQueueCluster):
     >>> from dask_jobqueue import SLURMCluster
     >>> cluster = SLURMCluster(
     ...     queue='regular',
-    ...     project="myproj",
+    ...     account="myaccount",
     ...     cores=24,
     ...     memory="500 GB"
     ... )
diff --git a/docs/source/configuration-setup.rst b/docs/source/configuration-setup.rst
index 34ba455..b89c6e7 100644
--- a/docs/source/configuration-setup.rst
+++ b/docs/source/configuration-setup.rst
@@ -100,19 +100,20 @@ based on their size and urgency.
 If you are unfamiliar with using queues on your system you should leave this
 blank, or ask your IT administrator.
 
-Project
--------
+Project or Account
+------------------
 
 You may have an allocation on your HPC system that is referenced by a
-*project*.  This is typically a short bit of text that references your group or
+*project* or *account*.  This is typically a short bit of text that references your group or
 a particular project.  This is typically given to you by your IT administrator
 when they give you an allocation of hours on the HPC system.
 
 .. code-block:: yaml
 
    project: XYZW-1234
+   #account: XYZW-1234
 
-If this sounds foreign to you or if you don't use project codes then you should
+If this sounds foreign to you or if you don't use project or account codes then you should
 leave this blank, or ask your IT administrator.
 
 
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index eb867c6..8363e22 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -24,7 +24,7 @@ define a single job:
         # Job scheduler specific keywords
         resource_spec='select=1:ncpus=24:mem=100GB',
         queue='regular',
-        project='my-project',
+        account='my-account',
         walltime='02:00:00',
    )
 
@@ -68,7 +68,7 @@ recommend using a configuration file like the following:
 
        resource-spec: "select=1:ncpus=24:mem=100GB"
        queue: regular
-       project: my-project
+       account: my-account
        walltime: 00:30:00
 
 See :doc:`Configuration Examples <configurations>` for real-world examples.
diff --git a/docs/source/configurations.rst b/docs/source/configurations.rst
index 69104a1..b26ffa2 100644
--- a/docs/source/configurations.rst
+++ b/docs/source/configurations.rst
@@ -50,7 +50,7 @@ Geyser/Caldera).
 
        interface: ib0
 
-       project: PXYZ123
+       account: PXYZ123
        walltime: '00:30:00'
        job-extra: {-C geyser}
 
@@ -107,7 +107,7 @@ ARM Stratus
         interface: ib0
         local-directory: $localscratch
         queue: high_mem # Can also select batch or gpu_ssd
-        project: arm
+        account: arm
         walltime: 00:30:00 #Adjust this to job size
         job-extra: ['-W group_list=cades-arm']
         
@@ -134,7 +134,7 @@ Also, note that port 8787 is open both on login and computing nodes, so you can
 
        # SLURM resource manager options
        queue: compute
-       # project: xxxxxxx # choose project other than default
+       # account: xxxxxxx # choose account other than default
        walltime: '00:30:00'
        job-mem: 120GB              # Max memory that can be requested to SLURM
 
@@ -175,7 +175,7 @@ about this ``dask-jobqueue`` config.
 
        # PBS resource manager options
        queue: mpi_1
-       project: myPROJ
+       account: myAccount
        walltime: '48:00:00'
        resource-spec: select=1:ncpus=28:mem=120GB
        # disable email
diff --git a/docs/source/examples.rst b/docs/source/examples.rst
index 118c57b..bb588da 100644
--- a/docs/source/examples.rst
+++ b/docs/source/examples.rst
@@ -14,7 +14,7 @@ PBS Deployments
    from dask_jobqueue import PBSCluster
 
    cluster = PBSCluster(queue='regular',
-                        project='DaskOnPBS',
+                        account='DaskOnPBS',
                         local_directory='$TMPDIR',
                         cores=24,
                         processes=6,
@@ -25,7 +25,7 @@ PBS Deployments
                         processes=6,
                         shebang='#!/usr/bin/env zsh',
                         memory="6GB",
-                        project='P48500028',
+                        account='P48500028',
                         queue='premium',
                         resource_spec='select=1:ncpus=36:mem=109G',
                         walltime='02:00:00',
@@ -45,7 +45,7 @@ can be used, called ``MoabCluster``:
    cluster = MoabCluster(
        cores=6,
        processes=6,
-       project="gfdl_m",
+       account="gfdl_m",
        memory="16G",
        resource_spec="pmem=96G",
        job_extra_directives=["-d /home/First.Last", "-M none"],
@@ -112,7 +112,7 @@ SLURM Deployments
    cluster = SLURMCluster(cores=8,
                           processes=4,
                           memory="16GB",
-                          project="woodshole",
+                          account="woodshole",
                           walltime="01:00:00",
                           queue="normal")
 
@@ -130,7 +130,7 @@ SLURM Deployment: Low-priority node usage
         cores=24,
         processes=6,
         memory="16GB",
-        project="co_laika",
+        account="co_laika",
         queue="savio2_bigmem",
         job_script_prologue=[
             'export LANG="en_US.utf8"',
diff --git a/docs/source/howitworks.rst b/docs/source/howitworks.rst
index 282643d..18487d6 100644
--- a/docs/source/howitworks.rst
+++ b/docs/source/howitworks.rst
@@ -19,7 +19,7 @@ object is instantiated:
         local_directory='$TMPDIR',
         resource_spec='select=1:ncpus=24:mem=100GB',
         queue='regular',
-        project='my-project',
+        account='my-account',
         walltime='02:00:00',
    )
 


Test patch:
diff --git a/dask_jobqueue/tests/test_pbs.py b/dask_jobqueue/tests/test_pbs.py
index 11f1c04..3959c86 100644
--- a/dask_jobqueue/tests/test_pbs.py
+++ b/dask_jobqueue/tests/test_pbs.py
@@ -28,7 +28,7 @@ def test_header(Cluster):
 
     with Cluster(
         queue="regular",
-        project="DaskOnPBS",
+        account="DaskOnPBS",
         processes=4,
         cores=8,
         resource_spec="select=1:ncpus=24:mem=100GB",
@@ -82,7 +82,7 @@ def test_job_script(Cluster):
 
     with Cluster(
         queue="regular",
-        project="DaskOnPBS",
+        account="DaskOnPBS",
         processes=4,
         cores=8,
         resource_spec="select=1:ncpus=24:mem=100GB",
@@ -347,7 +347,7 @@ def test_config(loop):
 def test_config_name_pbs_takes_custom_config():
     conf = {
         "queue": "myqueue",
-        "project": "myproject",
+        "account": "myaccount",
         "ncpus": 1,
         "cores": 1,
         "memory": "2 GB",
@@ -390,3 +390,67 @@ def test_informative_errors():
 async def test_adapt(loop):
     async with PBSCluster(cores=1, memory="1 GB", asynchronous=True) as cluster:
         cluster.adapt()
+
+
+def test_deprecation_project():
+    import warnings
+
+    # test issuing of warning
+    warnings.simplefilter("always")
+
+    job_cls = PBSCluster.job_cls
+    with warnings.catch_warnings(record=True) as w:
+        # should give a warning
+        job = job_cls(cores=1, memory="1 GB", project=["project is used"])
+        assert len(w) == 1
+        assert issubclass(w[0].category, FutureWarning)
+        assert "project has been renamed" in str(w[0].message)
+    with warnings.catch_warnings(record=True) as w:
+        # should give a warning
+        job = job_cls(
+            cores=1,
+            memory="1 GB",
+            project=["project is used"],
+            account=["account is used"],
+        )
+        assert len(w) == 1
+        assert issubclass(w[0].category, FutureWarning)
+        assert "project has been renamed" in str(w[0].message)
+    with warnings.catch_warnings(record=True) as w:
+        # should not give a warning
+        job = job_cls(
+            cores=1,
+            memory="1 GB",
+            account=["account is used, project not"],
+        )
+        assert len(w) == 0
+
+    # the rest is not about the warning but about behaviour: if account is not
+    # set, project should still be used if provided
+    warnings.simplefilter("ignore")
+    job = job_cls(
+        cores=1,
+        memory="1 GB",
+        project=["project"],
+        account=["account"],
+    )
+    job_script = job.job_script()
+    assert "project" not in job_script
+    assert "account" in job_script
+
+    job = job_cls(
+        cores=1,
+        memory="1 GB",
+        project=["project"],
+    )
+    job_script = job.job_script()
+    assert "project" in job_script
+
+    job = job_cls(
+        cores=1,
+        memory="1 GB",
+        project=["project"],
+        account=[],
+    )
+    job_script = job.job_script()
+    assert "project" in job_script
diff --git a/dask_jobqueue/tests/test_slurm.py b/dask_jobqueue/tests/test_slurm.py
index ae11e56..80769d5 100644
--- a/dask_jobqueue/tests/test_slurm.py
+++ b/dask_jobqueue/tests/test_slurm.py
@@ -28,7 +28,7 @@ def test_header():
 
     with SLURMCluster(
         queue="regular",
-        project="DaskOnSlurm",
+        account="DaskOnSlurm",
         processes=4,
         cores=8,
         memory="28GB",
@@ -179,7 +179,7 @@ def test_adaptive(loop):
 def test_config_name_slurm_takes_custom_config():
     conf = {
         "queue": "myqueue",
-        "project": "myproject",
+        "account": "myaccount",
         "ncpus": 1,
         "cores": 1,
         "memory": "2 GB",
@@ -249,3 +249,67 @@ def test_worker_name_uses_cluster_name(loop):
                 "test-my-env-variable-value-0",
                 "test-my-env-variable-value-1",
             ]
+
+
+def test_deprecation_project():
+    import warnings
+
+    # test issuing of warning
+    warnings.simplefilter("always")
+
+    job_cls = SLURMCluster.job_cls
+    with warnings.catch_warnings(record=True) as w:
+        # should give a warning
+        job = job_cls(cores=1, memory="1 GB", project=["project is used"])
+        assert len(w) == 1
+        assert issubclass(w[0].category, FutureWarning)
+        assert "project has been renamed" in str(w[0].message)
+    with warnings.catch_warnings(record=True) as w:
+        # should give a warning
+        job = job_cls(
+            cores=1,
+            memory="1 GB",
+            project=["project is used"],
+            account=["account is used"],
+        )
+        assert len(w) == 1
+        assert issubclass(w[0].category, FutureWarning)
+        assert "project has been renamed" in str(w[0].message)
+    with warnings.catch_warnings(record=True) as w:
+        # should not give a warning
+        job = job_cls(
+            cores=1,
+            memory="1 GB",
+            account=["account is used, project not"],
+        )
+        assert len(w) == 0
+
+    # the rest is not about the warning but about behaviour: if account is not
+    # set, project should still be used if provided
+    warnings.simplefilter("ignore")
+    job = job_cls(
+        cores=1,
+        memory="1 GB",
+        project=["project"],
+        account=["account"],
+    )
+    job_script = job.job_script()
+    assert "project" not in job_script
+    assert "account" in job_script
+
+    job = job_cls(
+        cores=1,
+        memory="1 GB",
+        project=["project"],
+    )
+    job_script = job.job_script()
+    assert "project" in job_script
+
+    job = job_cls(
+        cores=1,
+        memory="1 GB",
+        project=["project"],
+        account=[],
+    )
+    job_script = job.job_script()
+    assert "project" in job_script