From aaf9c5b3ab8393e07ad8eaf1d4955784b2fe69e1 Mon Sep 17 00:00:00 2001 From: MARCHAND MANON Date: Mon, 3 Jun 2024 15:10:00 +0200 Subject: [PATCH] feat: reduce API changes --- astroquery/simbad/core.py | 276 ++++++++++++++---- astroquery/simbad/criteria_lextab.py | 2 +- astroquery/simbad/criteria_parsetab.py | 23 +- astroquery/simbad/tests/test_simbad.py | 119 ++++++-- astroquery/simbad/tests/test_simbad_remote.py | 21 +- astroquery/simbad/tests/test_utils.py | 23 +- astroquery/simbad/utils.py | 109 ++++--- docs/simbad/simbad.rst | 16 +- docs/simbad/simbad_evolution.rst | 16 +- 9 files changed, 436 insertions(+), 169 deletions(-) diff --git a/astroquery/simbad/core.py b/astroquery/simbad/core.py index 4311bb2afd..7adefcd054 100644 --- a/astroquery/simbad/core.py +++ b/astroquery/simbad/core.py @@ -6,23 +6,22 @@ from difflib import get_close_matches from functools import lru_cache import gc -import json +import re from typing import Any -from pathlib import Path import warnings import astropy.coordinates as coord from astropy.table import Table, Column, vstack import astropy.units as u -from astropy.utils import isiterable -from astropy.utils.data import get_pkg_data_filename +from astropy.utils import isiterable, deprecated from astropy.utils.decorators import deprecated_renamed_argument from astroquery.query import BaseVOQuery -from astroquery.utils import commons, async_to_sync +from astroquery.utils import commons from astroquery.exceptions import LargeQueryWarning from astroquery.simbad.utils import (_catch_deprecated_fields_with_arguments, - _wildcard_to_regexp) + _wildcard_to_regexp, CriteriaTranslator, + query_criteria_fields) from pyvo.dal import TAPService from . import conf @@ -30,9 +29,6 @@ __all__ = ['Simbad', 'SimbadClass'] -with open(get_pkg_data_filename(str(Path("data") / "query_criteria_fields.json"))) as f: - query_criteria_fields = json.load(f) - def _adql_parameter(entry: str): """Replace single quotes by two single quotes. @@ -79,7 +75,6 @@ def _cached_query_tap(tap, query: str, *, maxrec=10000): return tap.search(query, maxrec=maxrec).to_table() -@async_to_sync class SimbadClass(BaseVOQuery): """The class for querying the SIMBAD web service. @@ -170,7 +165,16 @@ def hardlimit(self): @property def columns_in_output(self): - """A list of Simbad.Column.""" + """A list of Simbad.Column. + + They will be included in the output of the following methods: + - `~astroquery.simbad.SimbadClass.query_object`, + - `~astroquery.simbad.SimbadClass.query_objects`, + - `~astroquery.simbad.SimbadClass.query_region`, + - `~astroquery.simbad.SimbadClass.query_catalog`, + - `~astroquery.simbad.SimbadClass.query_bibobj`, + - `~astroquery.simbad.SimbadClass.query_criteria`. + """ if self._columns_in_output is None: self._columns_in_output = [Simbad.Column("basic", item) for item in conf.default_columns] @@ -180,11 +184,33 @@ def columns_in_output(self): def columns_in_output(self, list_columns): self._columns_in_output = list_columns + @staticmethod + def list_wildcards(): + """ + Displays the available wildcards that may be used in SIMBAD queries and + their usage. + + Examples + -------- + >>> from astroquery.simbad import Simbad + >>> Simbad.list_wildcards() + *: Any string of characters (including an empty one) + ?: Any character (exactly one character) + [abc]: Exactly one character taken in the list. Can also be defined by a range of characters: [A-Z] + [^0-9]: Any (one) character not in the list. + """ + WILDCARDS = {'*': 'Any string of characters (including an empty one)', + '?': 'Any character (exactly one character)', + '[abc]': ('Exactly one character taken in the list. ' + 'Can also be defined by a range of characters: [A-Z]'), + '[^0-9]': 'Any (one) character not in the list.'} + print("\n".join(f"{k}: {v}" for k, v in WILDCARDS.items())) + # --------------------------------- # Methods to define SIMBAD's output # --------------------------------- - def list_output_options(self): + def list_votable_fields(self): """List all options to add columns to SIMBAD's output. They are of three types: @@ -198,7 +224,7 @@ def list_output_options(self): Examples -------- >>> from astroquery.simbad import Simbad - >>> options = Simbad.list_output_options() # doctest: +REMOTE_DATA + >>> options = Simbad.list_votable_fields() # doctest: +REMOTE_DATA >>> # to print only the available bundles of columns >>> options[options["type"] == "bundle of basic columns"][["name", "description"]] # doctest: +REMOTE_DATA @@ -245,7 +271,7 @@ def _get_bundle_columns(self, bundle_name): Parameters ---------- bundle_name : str - The possible values can be listed with `~astroquery.simbad.SimbadClass.list_output_options` + The possible values can be listed with `~astroquery.simbad.SimbadClass.list_votable_fields` Returns ------- @@ -306,18 +332,20 @@ def _add_table_to_output(self, table): self.joins += [Simbad.Join(table, Simbad.Column("basic", link["target_column"]), Simbad.Column(table, link["from_column"]))] - def add_output_columns(self, *args): + def add_votable_fields(self, *args): """Add columns to the output of a SIMBAD query. The list of possible arguments and their description for this method - can be printed with `~astroquery.simbad.SimbadClass.list_output_options`. + can be printed with `~astroquery.simbad.SimbadClass.list_votable_fields`. The methods affected by this property are: - - `~astroquery.simbad.SimbadClass.query_object` - - `~astroquery.simbad.SimbadClass.query_objects` - - `~astroquery.simbad.SimbadClass.query_region` - - `~astroquery.simbad.SimbadClass.query_bibobj` + - `~astroquery.simbad.SimbadClass.query_object`, + - `~astroquery.simbad.SimbadClass.query_objects`, + - `~astroquery.simbad.SimbadClass.query_region`, + - `~astroquery.simbad.SimbadClass.query_catalog`, + - `~astroquery.simbad.SimbadClass.query_bibobj`, + - `~astroquery.simbad.SimbadClass.query_criteria`. Parameters @@ -329,15 +357,32 @@ def add_output_columns(self, *args): -------- >>> from astroquery.simbad import Simbad >>> simbad = Simbad() - >>> simbad.add_output_columns('sp_type', 'sp_qual', 'sp_bibcode') # doctest: +REMOTE_DATA + >>> simbad.add_votable_fields('sp_type', 'sp_qual', 'sp_bibcode') # doctest: +REMOTE_DATA >>> simbad.columns_in_output[0] # doctest: +REMOTE_DATA SimbadClass.Column(table='basic', name='main_id', alias=None) """ + + # the legacy way of adding fluxes is the only case-dependant option + args = list(args) + for arg in args: + if re.match(r"^flux.*\(.+\)$", arg): + warnings.warn("The notation 'flux(X)' is deprecated since 0.4.8. " + "See section on filters in " + "https://astroquery.readthedocs.io/en/latest/simbad/simbad_evolution.html " + "to see how it can be replaced.", DeprecationWarning, stacklevel=2) + flux_filter = re.findall(r"\((\w+)\)", arg)[0] + if len(flux_filter) == 1 and flux_filter.islower(): + flux_filter = flux_filter + "_" + self.joins.append(self.Join("allfluxes", self.Column("basic", "oid"), + self.Column("allfluxes", "oidref"))) + self.columns_in_output.append(self.Column("allfluxes", flux_filter)) + args.remove(arg) + # casefold args args = set(map(str.casefold, args)) # output options - output_options = self.list_output_options() + output_options = self.list_votable_fields() output_options["name"] = list(map(str.casefold, list(output_options["name"]))) basic_columns = output_options[output_options["type"] == "column of basic"]["name"] all_tables = output_options[output_options["type"] == "table"]["name"] @@ -387,17 +432,55 @@ def add_output_columns(self, *args): "into a separate VizieR catalog. It is possible to query " "it with the `astroquery.vizier` module.") else: - # it could be also be one of the fields with arguments + # raise a ValueError on fields with arguments _catch_deprecated_fields_with_arguments(votable_field) # or a typo close_match = get_close_matches(votable_field, set(output_options["name"])) error_message = (f"'{votable_field}' is not one of the accepted options " - "which can be listed with 'list_output_options'.") + "which can be listed with 'list_votable_fields'.") if close_match != []: close_matches = "' or '".join(close_match) error_message += f" Did you mean '{close_matches}'?" raise ValueError(error_message) + def get_votable_fields(self): + """Display votable fields.""" + return [f"{column.table}.{column.name}" for column in self.columns_in_output] + + def reset_votable_fields(self): + """Reset the output of the query_*** methods to default. + + They will be included in the output of the following methods: + - `~astroquery.simbad.SimbadClass.query_object`, + - `~astroquery.simbad.SimbadClass.query_objects`, + - `~astroquery.simbad.SimbadClass.query_region`, + - `~astroquery.simbad.SimbadClass.query_catalog`, + - `~astroquery.simbad.SimbadClass.query_bibobj`, + - `~astroquery.simbad.SimbadClass.query_criteria`. + """ + self.columns_in_output = [Simbad.Column("basic", item) + for item in conf.default_columns] + self.joins = [] + self.criteria = [] + + def get_field_description(self, field_name): + """Displays a description of the VOTable field. + + This can be replaced by the output of `~astroquery.simbad.SimbadClass.list_votable_fields`. + + Examples + -------- + >>> from astroquery.simbad import Simbad + >>> options = Simbad.list_votable_fields() # doctest: +REMOTE_DATA + >>> description_dimensions = options[options["name"] == "dimensions"]["description"] # doctest: +REMOTE_DATA + >>> description_dimensions.data.data[0] # doctest: +REMOTE_DATA + 'all fields related to object dimensions' + + """ + options = self.list_votable_fields() + description = options[options["name"] == field_name]["description"] + return description.data.data[0] + # ------------- # Query methods # ------------- @@ -424,8 +507,10 @@ def query_object(self, object_name, *, wildcard=False, syntax in a single string. See example. get_adql : bool, defaults to False Returns the ADQL string instead of querying SIMBAD. - verbose : deprecated since 0.4.8 - get_query_payload : deprecated since 0.4.8 + verbose : Deprecated since 0.4.8 + get_query_payload : Deprecated since 0.4.8. The query payload is not available + anymore, but the ADQL string can be returned instead with the ``get_adql`` + argument. Returns ------- @@ -439,7 +524,7 @@ def query_object(self, object_name, *, wildcard=False, >>> from astroquery.simbad import Simbad >>> simbad = Simbad() - >>> simbad.add_output_columns("dim") # doctest: +REMOTE_DATA + >>> simbad.add_votable_fields("dim") # doctest: +REMOTE_DATA >>> result = simbad.query_object("m101") # doctest: +REMOTE_DATA >>> result["main_id", "ra", "dec", "galdim_majaxis", "galdim_minaxis", "galdim_bibcode"] # doctest: +REMOTE_DATA
@@ -509,8 +594,10 @@ def query_objects(self, object_names, *, wildcard=False, criteria=None, syntax in a single string. See example. get_adql : bool, defaults to False Returns the ADQL string instead of querying SIMBAD. - verbose : deprecated since 0.4.8 - get_query_payload : deprecated since 0.4.8 + verbose : Deprecated since 0.4.8 + get_query_payload : Deprecated since 0.4.8. The query payload is not available + anymore, but the ADQL string can be returned instead with the ``get_adql`` + argument. Returns ------- @@ -580,12 +667,16 @@ def query_region(self, coordinates, radius=2*u.arcmin, *, syntax in a single string. get_adql : bool, defaults to False Returns the ADQL string instead of querying SIMBAD. - equinox : deprecated since 0.4.8 + equinox : Deprecated since 0.4.8 Use `~astropy.coordinates` objects instead - epoch : deprecated since 0.4.8 + epoch : Deprecated since 0.4.8 Use `~astropy.coordinates` objects instead - get_query_payload : deprecated since 0.4.8 - cache : deprecated since 0.4.8 + get_query_payload : Deprecated since 0.4.8. The query payload is not available + anymore, but the ADQL string can be returned instead with the ``get_adql`` + argument. + cache : Deprecated since 0.4.8. The cache is now automatically emptied at the + end of the python session. It can also be emptied manually with + `~astroquery.simbad.SimbadClass.clear_cache` but cannot be deactivated. Returns ------- @@ -601,7 +692,7 @@ def query_region(self, coordinates, radius=2*u.arcmin, *, >>> from astropy.coordinates import SkyCoord >>> simbad = Simbad() >>> simbad.ROW_LIMIT = 5 - >>> simbad.add_output_columns("otype") # doctest: +REMOTE_DATA + >>> simbad.add_votable_fields("otype") # doctest: +REMOTE_DATA >>> coordinates = SkyCoord([SkyCoord(186.6, 12.7, unit=("deg", "deg")), ... SkyCoord(170.75, 23.9, unit=("deg", "deg"))]) >>> result = simbad.query_region(coordinates, radius="2d5m", @@ -687,9 +778,13 @@ def query_catalog(self, catalog, *, criteria=None, get_adql=False, syntax in a single string. See example. get_adql : bool, defaults to False Returns the ADQL string instead of querying SIMBAD. - verbose : deprecated since 0.4.8 - get_query_payload : deprecated since 0.4.8 - cache : deprecated since 0.4.8 + verbose : Deprecated since 0.4.8 + get_query_payload : Deprecated since 0.4.8. The query payload is not available + anymore, but the ADQL string can be returned instead with the ``get_adql`` + argument. + cache : Deprecated since 0.4.8. The cache is now automatically emptied at the + end of the python session. It can also be emptied manually with + `~astroquery.simbad.SimbadClass.clear_cache` but cannot be deactivated. Returns ------- @@ -745,11 +840,10 @@ def query_bibobj(self, bibcode, *, criteria=None, ---------- bibcode : str the bibcode of the article - get_query_payload : bool, optional - When set to `True` the method returns the HTTP request parameters. - Defaults to `False`. - verbose : deprecated since 0.4.8 - get_query_payload : deprecated since 0.4.8 + get_query_payload : Deprecated since 0.4.8. The query payload is not available + anymore, but the ADQL string can be returned instead with the ``get_adql`` + argument. + verbose : Deprecated since 0.4.8 Returns ------- @@ -799,11 +893,13 @@ def query_bibcode(self, bibcode, *, wildcard=False, criteria : str Criteria to be applied to the query. These should be written in the ADQL syntax in a single string. See example. - verbose : deprecated since 0.4.8 - get_query_payload : deprecated since 0.4.8 - cache : The cache is now bound to the python session. It can be emptied with - `~astroquery.simbad.SimbadClass.empty_cache()` but cannot be deactivated - from here. Deprecated since 0.4.8 + verbose : Deprecated since 0.4.8 + get_query_payload : Deprecated since 0.4.8. The query payload is not available + anymore, but the ADQL string can be returned instead with the ``get_adql`` + argument. + cache : Deprecated since 0.4.8. The cache is now automatically emptied at the + end of the python session. It can also be emptied manually with + `~astroquery.simbad.SimbadClass.clear_cache` but cannot be deactivated. Returns ------- @@ -873,9 +969,13 @@ def query_objectids(self, object_name, *, verbose=None, cache=None, the column ``ident.id``. get_adql : bool, optional Returns the ADQL string instead of querying SIMBAD, by default False. - verbose : deprecated since 0.4.8 - get_query_payload : deprecated since 0.4.8 - cache : deprecated since 0.4.8 + verbose : Deprecated since 0.4.8 + get_query_payload : Deprecated since 0.4.8. The query payload is not available + anymore, but the ADQL string can be returned instead with the ``get_adql`` + argument. + cache : Deprecated since 0.4.8. The cache is now automatically emptied at the + end of the python session. It can also be emptied manually with + `~astroquery.simbad.SimbadClass.clear_cache` but cannot be deactivated. Returns ------- @@ -923,6 +1023,80 @@ def query_objectids(self, object_name, *, verbose=None, cache=None, return query return self.query_tap(query) + @deprecated(since="v0.4.8", + message=("'query_criteria' is deprecated. It uses the former sim-script " + "(SIMBAD specific) syntax " + "(see https://simbad.cds.unistra.fr/simbad/sim-fsam). " + "Possible replacements are the 'criteria' argument in the other " + "query methods or custom 'query_tap' queries. " + "These two replacements use the standard ADQL syntax.")) + def query_criteria(self, *args, get_adql=False, **kwargs): + """Query SIMBAD based on any criteria [deprecated]. + + This method is deprecated as it uses the former SIMBAD-specific sim-script syntax. + There are two possible replacements that have been added with astroquery v0.4.8 + and that use the standard ADQL syntax. See the examples section. + + Parameters + ---------- + args: + String arguments passed directly to SIMBAD's script + (e.g., 'region(box, GAL, 10.5 -10.5, 0.5d 0.5d)') + kwargs: + Keyword / value pairs passed to SIMBAD's script engine + (e.g., {'otype'='SNR'} will be rendered as otype=SNR) + + Returns + ------- + table : `~astropy.table.Table` + Query results table + + Examples + -------- + + Can be replaced by the ``criteria`` argument that was added in the + other query_*** methods + + >>> from astroquery.simbad import Simbad + >>> Simbad(ROW_LIMIT=5).query_region('M1', '2d', criteria="otype='G..'") # doctest: +REMOTE_DATA +IGNORE_OUTPUT +
+ main_id ra ... coo_wavelength coo_bibcode + deg ... + object float64 ... str1 object + ------------ ----------------- ... -------------- ------------------- + LEDA 136099 85.48166666666667 ... 1996A&AS..117....1S + LEDA 136047 83.66958333333332 ... 1996A&AS..117....1S + LEDA 136057 84.64499999999998 ... 1996A&AS..117....1S + LEDA 1630996 83.99208333333333 ... O 2003A&A...412...45P + 2MFGC 4574 84.37534166666669 ... I 2006AJ....131.1163S + + Or by custom-written ADQL queries + + >>> from astroquery.simbad import Simbad + >>> Simbad.query_tap("SELECT TOP 5 main_id, sp_type" + ... " FROM basic WHERE sp_type < 'F3'") # doctest: +REMOTE_DATA +
+ main_id sp_type + object object + ----------- ------- + HD 24033B (A) + HD 70218B (A) + HD 128284B (A/F) + CD-34 5319 (A/F) + HD 80593 (A0)V + """ + top, columns, joins, instance_criteria = self._get_query_parameters() + list_kwargs = [f"{key}='{argument}'" for key, argument in kwargs.items()] + added_criteria = f"({CriteriaTranslator.parse(' & '.join(list(list(args) + list_kwargs)))})" + instance_criteria.append(added_criteria) + if "otypes." in added_criteria: + joins.append(self.Join("otypes", self.Column("basic", "oid"), + self.Column("otypes", "oidref"))) + if "allfluxes." in added_criteria: + joins.append(self.Join("allfluxes", self.Column("basic", "oid"), + self.Column("allfluxes", "oidref"))) + return self._construct_query(top, columns, joins, instance_criteria, get_adql) + def list_tables(self, *, get_adql=False): """List the names and descriptions of the tables in SIMBAD. @@ -1209,6 +1383,8 @@ def _construct_query(self, top, columns, joins, criteria, get_adql=False, **uplo if joins == []: join = "" else: + unique_joins = [] + [unique_joins.append(join) for join in joins if join not in unique_joins] join = " " + " ".join([(f'{join.join_type} {join.table} ON {join.column_left.table}."' f'{join.column_left.name}" = {join.column_right.table}."' f'{join.column_right.name}"') for join in joins]) diff --git a/astroquery/simbad/criteria_lextab.py b/astroquery/simbad/criteria_lextab.py index 00b2fb3ef2..5166b08e76 100644 --- a/astroquery/simbad/criteria_lextab.py +++ b/astroquery/simbad/criteria_lextab.py @@ -15,7 +15,7 @@ _lexreflags = 34 _lexliterals = '&\\|\\(\\)' _lexstateinfo = {'INITIAL': 'inclusive'} -_lexstatere = {'INITIAL': [("(?Pin\\b)|(?P\\( *'[^\\)]*\\))|(?P>=|<=|!=|>|<|=)|(?P~|∼)|(?P!~|!∼)|(?P'[^']*')|(?Pregion\\([^\\)]*\\))|(?P[a-zA-Z_][a-zA-Z_0-9]*)|(?P\\d*\\.?\\d+)", [None, ('t_IN', 'IN'), ('t_LIST', 'LIST'), ('t_BINARY_OPERATOR', 'BINARY_OPERATOR'), ('t_LIKE', 'LIKE'), ('t_NOTLIKE', 'NOTLIKE'), ('t_STRING', 'STRING'), ('t_REGION', 'REGION'), ('t_COLUMN', 'COLUMN'), (None, 'NUMBER')])]} +_lexstatere = {'INITIAL': [("(?Pin\\b)|(?P\\( *'[^\\)]*\\))|(?P>=|<=|!=|>|<|=)|(?P~|∼)|(?P!~|!∼)|(?P'[^']*')|(?Pregion\\([^\\)]*\\))|(?P[a-zA-Z_*][a-zA-Z_0-9*]*)|(?P\\d*\\.?\\d+)", [None, ('t_IN', 'IN'), ('t_LIST', 'LIST'), ('t_BINARY_OPERATOR', 'BINARY_OPERATOR'), ('t_LIKE', 'LIKE'), ('t_NOTLIKE', 'NOTLIKE'), ('t_STRING', 'STRING'), ('t_REGION', 'REGION'), ('t_COLUMN', 'COLUMN'), (None, 'NUMBER')])]} _lexstateignore = {'INITIAL': ', \t\n'} _lexstateerrorf = {'INITIAL': 't_error'} _lexstateeoff = {} diff --git a/astroquery/simbad/criteria_parsetab.py b/astroquery/simbad/criteria_parsetab.py index ea3cb64864..0e00ea4fc4 100644 --- a/astroquery/simbad/criteria_parsetab.py +++ b/astroquery/simbad/criteria_parsetab.py @@ -17,9 +17,9 @@ _lr_method = 'LALR' -_lr_signature = "BINARY_OPERATOR COLUMN IN LIKE LIST NOTLIKE NUMBER REGION STRINGcriteria : criteria '|' criteriacriteria : criteria '&' criteriacriteria : '(' criteria ')'criteria : COLUMN BINARY_OPERATOR STRING\n | COLUMN BINARY_OPERATOR NUMBER\n criteria : COLUMN LIKE STRINGcriteria : COLUMN NOTLIKE STRINGcriteria : COLUMN IN LISTcriteria : REGION" +_lr_signature = "BINARY_OPERATOR COLUMN IN LIKE LIST NOTLIKE NUMBER REGION STRINGcriteria : criteria '|' criteriacriteria : criteria '&' criteriacriteria : '(' criteria ')'criteria : COLUMN BINARY_OPERATOR STRING\n | COLUMN BINARY_OPERATOR NUMBER\n | COLUMN IN LIST\n criteria : COLUMN BINARY_OPERATOR COLUMN\n criteria : COLUMN LIKE STRINGcriteria : COLUMN NOTLIKE STRINGcriteria : REGION" -_lr_action_items = {'(':([0,2,5,6,],[2,2,2,2,]),'COLUMN':([0,2,5,6,],[3,3,3,3,]),'REGION':([0,2,5,6,],[4,4,4,4,]),'$end':([1,4,12,13,14,15,16,17,18,19,],[0,-9,-1,-2,-3,-4,-5,-6,-7,-8,]),'|':([1,4,7,12,13,14,15,16,17,18,19,],[5,-9,5,5,5,-3,-4,-5,-6,-7,-8,]),'&':([1,4,7,12,13,14,15,16,17,18,19,],[6,-9,6,6,6,-3,-4,-5,-6,-7,-8,]),'BINARY_OPERATOR':([3,],[8,]),'LIKE':([3,],[9,]),'NOTLIKE':([3,],[10,]),'IN':([3,],[11,]),')':([4,7,12,13,14,15,16,17,18,19,],[-9,14,-1,-2,-3,-4,-5,-6,-7,-8,]),'STRING':([8,9,10,],[15,17,18,]),'NUMBER':([8,],[16,]),'LIST':([11,],[19,]),} +_lr_action_items = {'(':([0,2,5,6,],[2,2,2,2,]),'COLUMN':([0,2,5,6,8,],[3,3,3,3,15,]),'REGION':([0,2,5,6,],[4,4,4,4,]),'$end':([1,4,12,13,14,15,16,17,18,19,20,],[0,-10,-1,-2,-3,-7,-4,-5,-6,-8,-9,]),'|':([1,4,7,12,13,14,15,16,17,18,19,20,],[5,-10,5,5,5,-3,-7,-4,-5,-6,-8,-9,]),'&':([1,4,7,12,13,14,15,16,17,18,19,20,],[6,-10,6,6,6,-3,-7,-4,-5,-6,-8,-9,]),'BINARY_OPERATOR':([3,],[8,]),'IN':([3,],[9,]),'LIKE':([3,],[10,]),'NOTLIKE':([3,],[11,]),')':([4,7,12,13,14,15,16,17,18,19,20,],[-10,14,-1,-2,-3,-7,-4,-5,-6,-8,-9,]),'STRING':([8,10,11,],[16,19,20,]),'NUMBER':([8,],[17,]),'LIST':([9,],[18,]),} _lr_action = {} for _k, _v in _lr_action_items.items(): @@ -38,13 +38,14 @@ del _lr_goto_items _lr_productions = [ ("S' -> criteria","S'",1,None,None,None), - ('criteria -> criteria | criteria','criteria',3,'p_criteria_OR','utils.py',374), - ('criteria -> criteria & criteria','criteria',3,'p_criteria_AND','utils.py',378), - ('criteria -> ( criteria )','criteria',3,'p_criteria_parenthesis','utils.py',382), - ('criteria -> COLUMN BINARY_OPERATOR STRING','criteria',3,'p_criteria_string','utils.py',386), - ('criteria -> COLUMN BINARY_OPERATOR NUMBER','criteria',3,'p_criteria_string','utils.py',387), - ('criteria -> COLUMN LIKE STRING','criteria',3,'p_criteria_like','utils.py',392), - ('criteria -> COLUMN NOTLIKE STRING','criteria',3,'p_criteria_notlike','utils.py',396), - ('criteria -> COLUMN IN LIST','criteria',3,'p_criteria_in','utils.py',400), - ('criteria -> REGION','criteria',1,'p_criteria_region','utils.py',404), + ('criteria -> criteria | criteria','criteria',3,'p_criteria_OR','utils.py',298), + ('criteria -> criteria & criteria','criteria',3,'p_criteria_AND','utils.py',302), + ('criteria -> ( criteria )','criteria',3,'p_criteria_parenthesis','utils.py',306), + ('criteria -> COLUMN BINARY_OPERATOR STRING','criteria',3,'p_criteria_string','utils.py',310), + ('criteria -> COLUMN BINARY_OPERATOR NUMBER','criteria',3,'p_criteria_string','utils.py',311), + ('criteria -> COLUMN IN LIST','criteria',3,'p_criteria_string','utils.py',312), + ('criteria -> COLUMN BINARY_OPERATOR COLUMN','criteria',3,'p_criteria_string_no_ticks','utils.py',317), + ('criteria -> COLUMN LIKE STRING','criteria',3,'p_criteria_like','utils.py',323), + ('criteria -> COLUMN NOTLIKE STRING','criteria',3,'p_criteria_notlike','utils.py',327), + ('criteria -> REGION','criteria',1,'p_criteria_region','utils.py',331), ] diff --git a/astroquery/simbad/tests/test_simbad.py b/astroquery/simbad/tests/test_simbad.py index 14464caf06..f3ff3b1ba6 100644 --- a/astroquery/simbad/tests/test_simbad.py +++ b/astroquery/simbad/tests/test_simbad.py @@ -30,10 +30,10 @@ def _mock_simbad_class(monkeypatch): table = parse_single_table(f).to_table() # This should not change too often, to regenerate this file, do: # >>> from astroquery.simbad import Simbad - # >>> options = Simbad.list_output_options() + # >>> options = Simbad.list_votable_fields() # >>> options.write("simbad_output_options.xml", format="votable") monkeypatch.setattr(simbad.SimbadClass, "hardlimit", 2000000) - monkeypatch.setattr(simbad.SimbadClass, "list_output_options", lambda self: table) + monkeypatch.setattr(simbad.SimbadClass, "list_votable_fields", lambda self: table) @pytest.fixture() @@ -50,7 +50,7 @@ def _mock_list_columns(self, table_name=None): """Patch a call with basic as an argument only.""" if table_name == "basic": return table - # to test in add_output_columns + # to test in add_votable_fields if table_name == "mesdistance": return Table( [["bibcode"]], names=["column_name"] @@ -146,8 +146,8 @@ def test_init_columns_in_output(): @pytest.mark.usefixtures("_mock_simbad_class") def test_mocked_simbad(): simbad_instance = simbad.Simbad() - # this mocks the list_output_options - options = simbad_instance.list_output_options() + # this mocks the list_votable_fields + options = simbad_instance.list_votable_fields() assert len(options) > 90 # this mocks the hardlimit assert simbad_instance.hardlimit == 2000000 @@ -158,16 +158,39 @@ def test_mocked_simbad(): @pytest.mark.usefixtures("_mock_basic_columns") -def test_list_output_options(monkeypatch): +def test_votable_fields_utils(monkeypatch): monkeypatch.setattr(simbad.SimbadClass, "query_tap", lambda self, _: Table([["biblio"], ["biblio description"]], names=["name", "description"], dtype=["object", "object"])) - options = simbad.SimbadClass().list_output_options() + options = simbad.SimbadClass().list_votable_fields() assert set(options.group_by("type").groups.keys["type"]) == {"table", "column of basic", "bundle of basic columns"} + description = simbad.SimbadClass().get_field_description("velocity") + assert description == 'all fields related with radial velocity and redshift' + fields = simbad.SimbadClass().get_votable_fields() + expected_fields = [ + 'basic.main_id', 'basic.ra', 'basic.dec', 'basic.coo_err_maj', + 'basic.coo_err_min', 'basic.coo_err_angle', 'basic.coo_wavelength', + 'basic.coo_bibcode' + ] + assert fields == expected_fields + + +@pytest.mark.usefixtures("_mock_simbad_class") +@pytest.mark.usefixtures("_mock_basic_columns") +@pytest.mark.usefixtures("_mock_linked_to_basic") +def test_reset_votable_fields(): + simbad_instance = simbad.Simbad() + # add one + simbad_instance.add_votable_fields("otype") + assert simbad.Simbad.Column("basic", "otype") in simbad_instance.columns_in_output + # reset + simbad_instance.reset_votable_fields() + assert not simbad.Simbad.Column("basic", "otype") in simbad_instance.columns_in_output + @pytest.mark.usefixtures("_mock_basic_columns") @pytest.mark.parametrize(("bundle_name", "column"), @@ -204,52 +227,60 @@ def test_add_table_to_output(monkeypatch): @pytest.mark.usefixtures("_mock_simbad_class") @pytest.mark.usefixtures("_mock_basic_columns") @pytest.mark.usefixtures("_mock_linked_to_basic") -def test_add_output_columns(): +def test_add_votable_fields(): simbad_instance = simbad.Simbad() # add columns from basic (one value) - simbad_instance.add_output_columns("pmra") + simbad_instance.add_votable_fields("pmra") assert simbad.SimbadClass.Column("basic", "pmra") in simbad_instance.columns_in_output # add two columns from basic - simbad_instance.add_output_columns("pmdec", "pm_bibcodE") # also test case insensitive + simbad_instance.add_votable_fields("pmdec", "pm_bibcodE") # also test case insensitive expected = [simbad.SimbadClass.Column("basic", "pmdec"), simbad.SimbadClass.Column("basic", "pm_bibcode")] assert all(column in simbad_instance.columns_in_output for column in expected) # add a table simbad_instance.columns_in_output = [] - simbad_instance.add_output_columns("basic") + simbad_instance.add_votable_fields("basic") assert [simbad.SimbadClass.Column("basic", "*")] == simbad_instance.columns_in_output # add a bundle - simbad_instance.add_output_columns("dimensions") + simbad_instance.add_votable_fields("dimensions") assert simbad.SimbadClass.Column("basic", "galdim_majaxis") in simbad_instance.columns_in_output # a column which name has changed should raise a warning but still # be added under its new name simbad_instance.columns_in_output = [] with pytest.warns(DeprecationWarning, match=r"'id\(1\)' has been renamed 'main_id'. You'll see it " "appearing with its new name in the output table"): - simbad_instance.add_output_columns("id(1)") + simbad_instance.add_votable_fields("id(1)") assert simbad.SimbadClass.Column("basic", "main_id") in simbad_instance.columns_in_output # a table which name has changed should raise a warning too with pytest.warns(DeprecationWarning, match="'distance' has been renamed 'mesdistance'*"): - simbad_instance.add_output_columns("distance") + simbad_instance.add_votable_fields("distance") # errors are raised for the deprecated fields with options - with pytest.raises(ValueError, match="Criteria on filters are deprecated when defining Simbad's output.*"): - simbad_instance.add_to_output("fluxdata(V)") - with pytest.raises(ValueError, match="Coordinates conversion and formatting is no longer supported.*"): - simbad_instance.add_to_output("coo(s)", "dec(d)") + simbad_instance = simbad.SimbadClass() + with pytest.warns(DeprecationWarning, match=r"The notation \'flux\(X\)\' is deprecated since 0.4.8. *"): + simbad_instance.add_votable_fields("flux(u)") + assert "u_" in str(simbad_instance.columns_in_output) + with pytest.raises(ValueError, match="Coordinates conversion and formatting is no longer supported*"): + simbad_instance.add_votable_fields("coo(s)", "dec(d)") with pytest.raises(ValueError, match="Catalog Ids are no longer supported as an output option.*"): - simbad_instance.add_output_columns("ID(Gaia)") + simbad_instance.add_votable_fields("ID(Gaia)") with pytest.raises(ValueError, match="Selecting a range of years for bibcode is removed.*"): - simbad_instance.add_output_columns("bibcodelist(2042-2050)") + simbad_instance.add_votable_fields("bibcodelist(2042-2050)") # historical measurements with pytest.raises(ValueError, match="'einstein' is no longer a part of SIMBAD.*"): - simbad_instance.add_output_columns("einstein") + simbad_instance.add_votable_fields("einstein") # typos should have suggestions with pytest.raises(ValueError, match="'alltype' is not one of the accepted options which can be " - "listed with 'list_output_options'. Did you mean 'alltypes' or 'otype' or 'otypes'?"): - simbad_instance.add_output_columns("ALLTYPE") + "listed with 'list_votable_fields'. Did you mean 'alltypes' or 'otype' or 'otypes'?"): + simbad_instance.add_votable_fields("ALLTYPE") # bundles and tables require a connection to the tap_schema and are thus tested in test_simbad_remote +def test_list_wildcards(capsys): + simbad.SimbadClass.list_wildcards() + wildcards = capsys.readouterr() + assert "*: Any string of characters (including an empty one)" in wildcards.out + + # ------------------------------------------ # Test query_*** methods that call query_tap # ------------------------------------------ @@ -277,12 +308,13 @@ def test_query_bibcode_class(): @pytest.mark.usefixtures("_mock_simbad_class") def test_query_objectids(): - adql = simbad.core.Simbad.query_objectids('Polaris', - criteria="ident.id LIKE 'HD%'", - get_adql=True) - expected = ("SELECT ident.id FROM ident AS id_typed JOIN ident USING(oidref)" - "WHERE id_typed.id = 'Polaris' AND ident.id LIKE 'HD%'") - assert adql == expected + with pytest.raises(AstropyDeprecationWarning, match='"get_query_payload"*'): + adql = simbad.core.Simbad.query_objectids('Polaris', + criteria="ident.id LIKE 'HD%'", + get_query_payload=True) + expected = ("SELECT ident.id FROM ident AS id_typed JOIN ident USING(oidref)" + "WHERE id_typed.id = 'Polaris' AND ident.id LIKE 'HD%'") + assert adql == expected @pytest.mark.usefixtures("_mock_simbad_class") @@ -392,6 +424,35 @@ def test_query_object(): end = "AND (otype = 'G..')" assert adql.endswith(end) +# ------------------------ +# Tests for query_criteria +# ------------------------ + + +@pytest.mark.usefixtures("_mock_simbad_class") +def test_query_criteria(): + with pytest.warns(AstropyDeprecationWarning, match="'query_criteria' is deprecated*"): + # with a region and otype criteria + adql = simbad.core.Simbad.query_criteria("region(box, GAL, 49.89 -0.3, 0.5d 0.5d)", + otype='HII', get_adql=True) + expected = ("SELECT basic.\"main_id\", basic.\"ra\", basic.\"dec\", " + "basic.\"coo_err_maj\", basic.\"coo_err_min\", " + "basic.\"coo_err_angle\", basic.\"coo_wavelength\", " + "basic.\"coo_bibcode\" FROM basic JOIN otypes ON basic.\"oid\" = " + "otypes.\"oidref\" WHERE (CONTAINS(POINT('ICRS', ra, dec), " + "BOX('ICRS', 291.04898804231215, 14.903593816641127, 0.5, 0.5)) = 1 " + "AND otypes.otype = 'HII')") + assert adql == expected + # with a flux criteria + adql = simbad.core.Simbad.query_criteria("Umag < 9", get_adql=True) + expected = ( + 'SELECT basic."main_id", basic."ra", basic."dec", basic."coo_err_maj", ' + 'basic."coo_err_min", basic."coo_err_angle", basic."coo_wavelength", ' + 'basic."coo_bibcode" FROM basic JOIN allfluxes ON basic."oid" = ' + 'allfluxes."oidref" WHERE (allfluxes.U < 9)' + ) + assert adql == expected + # ------------------------- # Test query_tap exceptions # ------------------------- diff --git a/astroquery/simbad/tests/test_simbad_remote.py b/astroquery/simbad/tests/test_simbad_remote.py index 2a43ca6482..331867b262 100644 --- a/astroquery/simbad/tests/test_simbad_remote.py +++ b/astroquery/simbad/tests/test_simbad_remote.py @@ -3,6 +3,7 @@ from astropy.coordinates import SkyCoord import astropy.units as u +from astropy.utils.exceptions import AstropyDeprecationWarning from astropy.table import Table from astroquery.simbad import Simbad @@ -40,7 +41,7 @@ def test_non_ascii_bibcode(self): def test_query_bibobj(self): self.simbad.ROW_LIMIT = 5 - self.simbad.add_output_columns("otype") + self.simbad.add_votable_fields("otype") bibcode = '2005A&A...430..165F' result = self.simbad.query_bibobj(bibcode, criteria="otype='*..'") assert all((bibcode == code) for code in result["bibcode"].data.data) @@ -82,7 +83,7 @@ def test_query_multi_object(self): def test_simbad_flux_qual(self): '''Regression test for issue 680''' simbad_instance = Simbad() - simbad_instance.add_output_columns("flux") + simbad_instance.add_votable_fields("flux") response = simbad_instance.query_object('algol', criteria="filter='V'") # this is bugged, it should be "flux.qual", see https://github.com/gmantele/vollt/issues/154 # when the issue upstream in vollt (the TAP software used in SIMBAD) is fixed we can rewrite this test @@ -95,6 +96,14 @@ def test_query_object(self): result = self.simbad.query_object("NGC [0-9]*", wildcard=True) assert all(matched_id.startswith("NGC") for matched_id in result["matched_id"].data.data) + def test_query_criteria(self): + simbad_instance = Simbad() + simbad_instance.add_votable_fields("otype") + with pytest.warns(AstropyDeprecationWarning, match="'query_criteria' is deprecated*"): + result = simbad_instance.query_criteria("region(Galactic Center, 10s)", maintype="X") + assert all(result["otype"].data.data == "X") + assert len(result) >= 16 # there could be more measurements, there are 16 sources in 2024 + def test_query_tap(self): # a robust query about something that should not change in Simbad filtername = self.simbad.query_tap("select filtername from filter where filtername='B'") @@ -148,7 +157,7 @@ def test_add_bundle_to_output(self): # empty before the test simbad_instance.columns_in_output = [] # add a bundle - simbad_instance.add_output_columns("dim") + simbad_instance.add_votable_fields("dim") # check the length assert len(simbad_instance.columns_in_output) == 8 assert Simbad.Column("basic", "galdim_majaxis") in simbad_instance.columns_in_output @@ -157,7 +166,7 @@ def test_add_table_to_output(self): simbad_instance = Simbad() # empty before the test simbad_instance.columns_in_output = [] - simbad_instance.add_output_columns("otypes") + simbad_instance.add_votable_fields("otypes") assert Simbad.Column("otypes", "otype", '"otypes.otype"') in simbad_instance.columns_in_output # tables also require a join assert Simbad.Join("otypes", @@ -165,9 +174,9 @@ def test_add_table_to_output(self): Simbad.Column("otypes", "oidref")) == simbad_instance.joins[0] # tables that have been renamed should warn with pytest.warns(DeprecationWarning, match="'iue' has been renamed 'mesiue'.*"): - simbad_instance.add_output_columns("IUE") + simbad_instance.add_votable_fields("IUE") # empty before the test simbad_instance.columns_in_output = [] # mixed columns bundles and tables - simbad_instance.add_output_columns("flux", "velocity", "update_date") + simbad_instance.add_votable_fields("flux", "velocity", "update_date") assert len(simbad_instance.columns_in_output) == 19 diff --git a/astroquery/simbad/tests/test_utils.py b/astroquery/simbad/tests/test_utils.py index 1857037aeb..ce9e6dc455 100644 --- a/astroquery/simbad/tests/test_utils.py +++ b/astroquery/simbad/tests/test_utils.py @@ -2,7 +2,7 @@ import pytest from astroquery.simbad.utils import (CriteriaTranslator, _parse_coordinate_and_convert_to_icrs, - _region_to_contains, list_wildcards, _wildcard_to_regexp) + _region_to_contains, _wildcard_to_regexp) from astropy.coordinates.builtin_frames.icrs import ICRS from astropy.coordinates import SkyCoord @@ -18,12 +18,6 @@ def test_parse_coordinates_and_convert_to_icrs(coord_string, frame, epoch, equin assert isinstance(coord.frame, ICRS) -def test_list_wildcards(capsys): - list_wildcards() - wildcards = capsys.readouterr() - assert "*: Any string of characters (including an empty one)" in wildcards.out - - def test_wildcard_to_regexp(): # should add beginning and end operators, and translate * into .* assert _wildcard_to_regexp("test*") == "^test.*$" @@ -90,14 +84,21 @@ def test_tokenizer(): @pytest.mark.parametrize("test, result", [ ("region(GAL,180 0,2d) & otype = 'G' & (nbref >= 10|bibyear >= 2000)", ("CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', 86.40498828654475, 28.93617776179148, 2.0)) = 1" - " AND otype = 'G' AND (nbref >= 10 OR bibyear >= 2000)")), - ("otype != 'Galaxy..'", "otype != 'Galaxy..'"), + " AND otypes.otype = 'G' AND (nbref >= 10 OR bibyear >= 2000)")), ("author ∼ 'egret*'", "regexp(author, '^egret.*$') = 1"), ("cat in ('hd','hip','ppm')", "cat IN ('hd','hip','ppm')"), - ("author !~ 'test'", "regexp(author, '^test$') = 0") + ("author !~ 'test'", "regexp(author, '^test$') = 0"), + ("sptype < F4", "sp_type < 'F4'"), + ("umag < 1", "allfluxes.u_ < 1"), + ("Vmag = 10", "allfluxes.V = 10"), + ("otypes != 'Galaxy'", "otypes.otype != 'Galaxy..'"), + ("maintype=SNR", "basic.otype = 'SNR'"), + ("maintypes=SNR", "basic.otype = 'SNR..'") ]) # these are the examples from http://simbad.cds.unistra.fr/guide/sim-fsam.htx +# plus added examples def test_transpiler(test, result): - # to regenerate transpiler after a change in utils.py, delete `criteria_parsetab.py` and run this test file again. + # to regenerate transpiler after a change in utils.py, delete `criteria_parsetab.py` + # and run this test file again. translated = CriteriaTranslator.parse(test) assert translated == result diff --git a/astroquery/simbad/utils.py b/astroquery/simbad/utils.py index 8bcec086db..10586aa1d6 100644 --- a/astroquery/simbad/utils.py +++ b/astroquery/simbad/utils.py @@ -1,11 +1,17 @@ """Contains utility functions to support legacy Simbad interface.""" from collections import deque +import json +from pathlib import Path import re from astropy.coordinates import SkyCoord, Angle from astropy.utils.parsing import lex, yacc from astropy.utils import classproperty +from astropy.utils.data import get_pkg_data_filename + +with open(get_pkg_data_filename(str(Path("data") / "query_criteria_fields.json"))) as f: + query_criteria_fields = json.load(f) def _catch_deprecated_fields_with_arguments(votable_field): @@ -21,52 +27,22 @@ def _catch_deprecated_fields_with_arguments(votable_field): votable_field : str one of the former votable fields (see `~astroquery.simbad.SimbadClass.list_votable_fields`) """ - if re.match(r"^flux.*\(.+\)$", votable_field): - raise ValueError("Criteria on filters are deprecated when defining Simbad's output. " - "See section on filters in " - "https://astroquery.readthedocs.io/en/latest/simbad/simbad_evolution.html") if re.match(r"^(ra|dec|coo)\(.+\)$", votable_field): - raise ValueError("Coordinates conversion and formatting is no longer supported. This " - "can be done with the `~astropy.coordinates` module." + raise ValueError("Coordinates conversion and formatting is no longer supported within the " + "SIMBAD module. This can be done with the `~astropy.coordinates` module." "Coordinates are now per default in degrees and in the ICRS frame.") if votable_field.startswith("id("): raise ValueError("Catalog Ids are no longer supported as an output option. " - "A good replacement can be `~astroquery.simbad.SimbadClass.query_cat`. " - "See section on catalogs in " - "https://astroquery.readthedocs.io/en/latest/simbad/simbad_evolution.html") + "A good replacement can be `~astroquery.simbad.SimbadClass.query_cat`") if votable_field.startswith("bibcodelist("): raise ValueError("Selecting a range of years for bibcode is removed. You can still use " - "bibcodelist without parenthesis and get the full list of bibliographic references. " - "See https://astroquery.readthedocs.io/en/latest/simbad/simbad_evolution.html for " - "more details.") + "bibcodelist without parenthesis and get the full list of bibliographic references.") # ---------------------------- -# To support wildcard argument +# Support wildcard argument # ---------------------------- -def list_wildcards(): - """ - Displays the available wildcards that may be used in SIMBAD queries and - their usage. - - Examples - -------- - >>> from astroquery.simbad.utils import list_wildcards - >>> list_wildcards() - *: Any string of characters (including an empty one) - ?: Any character (exactly one character) - [abc]: Exactly one character taken in the list. Can also be defined by a range of characters: [A-Z] - [^0-9]: Any (one) character not in the list. - """ - WILDCARDS = {'*': 'Any string of characters (including an empty one)', - '?': 'Any character (exactly one character)', - '[abc]': ('Exactly one character taken in the list. ' - 'Can also be defined by a range of characters: [A-Z]'), - '[^0-9]': 'Any (one) character not in the list.'} - print("\n".join(f"{k}: {v}" for k, v in WILDCARDS.items())) - - def _wildcard_to_regexp(wildcard_string): r"""Translate a wildcard string into a regexp. @@ -109,6 +85,10 @@ def _wildcard_to_regexp(wildcard_string): # start and end of string + whitespace means any number of whitespaces return f"^{wildcard_string.replace(' ', ' +')}$" +# ---------------------------------------- +# Support legacy sim-script query language +# ---------------------------------------- + def _region_to_contains(region_string): """Translate a region string into an ADQL CONTAINS clause. @@ -201,6 +181,41 @@ def _parse_coordinate_and_convert_to_icrs(string_coordinate, *, return center.transform_to("icrs") +def _convert_column(column, operator=None, value=None): + """Convert columns from the sim-script language into ADQL. + + This checks the criteria names for fields that changed names between + sim-script and SIMBAD TAP (the old and new SIMBAD APIs). There are two exceptions + for magnitudes and fluxes where in sim-script the argument that was used in the criteria + was different from the name that wes used in votable_field (ex: flux(V) to add the + column and Vmag to add in a criteria). + """ + # handle the change of syntax on otypes manually because they are difficult to automatize + if column == "maintype": + column = "basic.otype" + elif column == "otype": + column = "otypes.otype" + elif column == "maintypes": + column = "basic.otype" + value = f"{value[:-1]}..'" + elif column == "otypes": + column = "otypes.otype" + value = f"{value[:-1]}..'" + # magnitudes are also an exception + elif "mag" in column: + column = column.replace("mag", "") + if len(column) == 1 and column.islower(): + column = column + "_" + column = "allfluxes." + column + # the other cases are a simple replacement by the new name + elif column in query_criteria_fields: + if query_criteria_fields[column]["type"] == "alias": + column = query_criteria_fields[column]["tap_column"] + if operator and value: + return column + " " + operator + " " + value + return column + + class CriteriaTranslator: _tokens = [ @@ -245,7 +260,7 @@ def t_BINARY_OPERATOR(t): return t def t_LIKE(t): - r"~|∼" # the examples in SIMBAD documentation use the strange long ∼ + r"~|∼" # the examples in SIMBAD documentation use this glyph '∼' t.value = "LIKE" return t @@ -264,7 +279,7 @@ def t_REGION(t): return t def t_COLUMN(t): - r'[a-zA-Z_][a-zA-Z_0-9]*' + r'[a-zA-Z_*][a-zA-Z_0-9*]*' return t t_ignore = ", \t\n" # noqa: F841 @@ -296,27 +311,31 @@ def p_criteria_parenthesis(p): def p_criteria_string(p): """criteria : COLUMN BINARY_OPERATOR STRING | COLUMN BINARY_OPERATOR NUMBER + | COLUMN IN LIST + """ + p[0] = _convert_column(p[1], p[2], p[3]) + + def p_criteria_string_no_ticks(p): + """criteria : COLUMN BINARY_OPERATOR COLUMN """ - p[0] = p[1] + " " + p[2] + " " + p[3] + # sim-script also tolerates omitting the '' at the right side of operators + p[0] = _convert_column(p[1], p[2], f"'{p[3]}'") def p_criteria_like(p): """criteria : COLUMN LIKE STRING""" - p[0] = "regexp(" + p[1] + ", '" + _wildcard_to_regexp(p[3][1:-1]) + "') = 1" + p[0] = "regexp(" + _convert_column(p[1]) + ", '" + _wildcard_to_regexp(p[3][1:-1]) + "') = 1" def p_criteria_notlike(p): """criteria : COLUMN NOTLIKE STRING""" - p[0] = "regexp(" + p[1] + ", '" + _wildcard_to_regexp(p[3][1:-1]) + "') = 0" - - def p_criteria_in(p): - """criteria : COLUMN IN LIST""" - p[0] = p[1] + " IN " + p[3] + p[0] = "regexp(" + _convert_column(p[1]) + ", '" + _wildcard_to_regexp(p[3][1:-1]) + "') = 0" def p_criteria_region(p): """criteria : REGION""" p[0] = _region_to_contains(p[1]) def p_error(p): - raise ValueError("Syntax error for sim-script criteria") + raise ValueError(f"Syntax error for sim-script criteria at line {p.lineno}" + f" character {p.lexpos - 1}") return yacc(tabmodule="criteria_parsetab", package="astroquery/simbad") diff --git a/docs/simbad/simbad.rst b/docs/simbad/simbad.rst index 60a4733cd8..990b9826c2 100644 --- a/docs/simbad/simbad.rst +++ b/docs/simbad/simbad.rst @@ -113,8 +113,8 @@ To see the available wildcards and their meaning: .. code-block:: python - >>> from astroquery.simbad.utils import list_wildcards - >>> list_wildcards() + >>> from astroquery.simbad import Simbad + >>> Simbad().list_wildcards() *: Any string of characters (including an empty one) ?: Any character (exactly one character) [abc]: Exactly one character taken in the list. Can also be defined by a range of characters: [A-Z] @@ -221,7 +221,7 @@ If the center is defined by coordinates, then the best solution is to use a >>> import astropy.units as u >>> Simbad.query_region(SkyCoord(ra=[10, 11], dec=[10, 11], ... unit=(u.deg, u.deg), frame='fk5'), - ... radius=[0.1 * u.deg, 2* u.arcmin]) + ... radius=[0.1 * u.deg, 2* u.arcmin]) # doctest: +IGNORE_OUTPUT
main_id ra ... coo_bibcode deg ... @@ -449,13 +449,13 @@ For these methods, the default columns in the output are: SimbadClass.Column(table='basic', name='main_id', alias=None) This can be permanently changed in astroquery's configuration files. To do this within -a session or for a single query, use `~astroquery.simbad.SimbadClass.add_output_columns`: +a session or for a single query, use `~astroquery.simbad.SimbadClass.add_votable_fields`: .. doctest-remote-data:: >>> from astroquery.simbad import Simbad >>> simbad = Simbad() - >>> simbad.add_output_columns("otype") # here we add a single column about the main object type + >>> simbad.add_votable_fields("otype") # here we add a single column about the main object type Some options add a single column and others add a bunch of columns that are relevant for a theme (ex: fluxes, proper motions...). The list of possible options is printed @@ -464,7 +464,7 @@ with: .. doctest-remote-data:: >>> from astroquery.simbad import Simbad - >>> Simbad.list_output_options()[["name", "description"]] + >>> Simbad.list_votable_fields()[["name", "description"]]
name description object object @@ -524,7 +524,7 @@ This allows to inspect the columns the method would return: >>> simbad = Simbad() >>> simbad.ROW_LIMIT = 0 # get no lines, just the table structure >>> # add the table about proper motion measurements, and the object type column - >>> simbad.add_output_columns("mesPM", "otype") + >>> simbad.add_votable_fields("mesPM", "otype") >>> peek = simbad.query_object("BD+30 2512") # a query on an object >>> peek.info
@@ -564,7 +564,7 @@ constraint on the first character of the ``mespm.bibcode`` column >>> from astroquery.simbad import Simbad >>> criteria = "mespm.bibcode LIKE '2%'" # starts with 2, anything after >>> simbad = Simbad() - >>> simbad.add_output_columns("mesPM", "otype") + >>> simbad.add_votable_fields("mesPM", "otype") >>> pm_measurements = simbad.query_object("BD+30 2512", criteria=criteria) >>> pm_measurements[["main_id", "mespm.pmra", "mespm.pmde", "mespm.bibcode"]]
diff --git a/docs/simbad/simbad_evolution.rst b/docs/simbad/simbad_evolution.rst index ae153ccbea..5b26a3a9fc 100644 --- a/docs/simbad/simbad_evolution.rst +++ b/docs/simbad/simbad_evolution.rst @@ -10,11 +10,11 @@ Votable fields and Output options Votable fields are deprecated in favor of output options. Most of the former votable fields can now be added to the output of Simbad queries with -`~astroquery.simbad.SimbadClass.add_output_columns`. The full list of options is available -with the `~astroquery.simbad.SimbadClass.list_output_options` method. +`~astroquery.simbad.SimbadClass.add_votable_fields`. The full list of options is available +with the `~astroquery.simbad.SimbadClass.list_votable_fields` method. Some columns and tables have a new name under the TAP interface. The old name will be -recognized by `~astroquery.simbad.SimbadClass.add_output_columns`, but only the new name will +recognized by `~astroquery.simbad.SimbadClass.add_votable_fields`, but only the new name will appear in the output. A few ``votable_fields`` had options in parenthesis. This is no longer supported and can @@ -90,7 +90,7 @@ that will work as ``criteria`` in the other query methods cited above: >>> from astroquery.simbad.utils import CriteriaTranslator >>> CriteriaTranslator.parse("region(box, GAL, 0 +0, 3d 1d) & otype='SNR'") - "CONTAINS(POINT('ICRS', ra, dec), BOX('ICRS', 266.4049882865447, -28.936177761791473, 3.0, 1.0)) = 1 AND otype = 'SNR'" + "CONTAINS(POINT('ICRS', ra, dec), BOX('ICRS', 266.4049882865447, -28.936177761791473, 3.0, 1.0)) = 1 AND otypes.otype = 'SNR'" This string can now be incorporated in any of the query methods that accept a ``criteria`` argument. @@ -104,10 +104,10 @@ See a more elaborated example: >>> from astroquery.simbad import Simbad >>> from astroquery.simbad.utils import CriteriaTranslator >>> # not a galaxy, and not a globular cluster - >>> old_criteria = "otype != 'Galaxy..' & otype != 'Cl*..'" + >>> old_criteria = "maintype != 'Galaxy..' & maintype != 'Cl*..'" >>> simbad = Simbad() >>> # we add the main type and all the types that have historically been attributed to the object - >>> simbad.add_output_columns("otype", "alltypes") + >>> simbad.add_votable_fields("otype", "alltypes") >>> result = simbad.query_catalog("M", criteria=CriteriaTranslator.parse(old_criteria)) >>> result.sort("catalog_id") >>> result[["main_id", "catalog_id", "otype", "otypes"]] @@ -252,7 +252,7 @@ The important information is in the column ``filtername``. You can now use this filter name in a criteria string. For example, to get fluxes for a specific object, one can use `~astroquery.simbad.SimbadClass.query_object` as a first base (it selects a single object by its name), add different fields to -the output with `~astroquery.simbad.SimbadClass.add_output_columns` (here ``flux`` adds all +the output with `~astroquery.simbad.SimbadClass.add_votable_fields` (here ``flux`` adds all columns about fluxes) and then select only the interesting filters with a ``criteria`` argument: @@ -263,7 +263,7 @@ argument: >>> from astroquery.simbad import Simbad >>> simbad = Simbad() - >>> simbad.add_output_columns("flux") + >>> simbad.add_votable_fields("flux") >>> result = simbad.query_object("BD-16 5701", criteria="filter IN ('U', 'B', 'G')") >>> result[["main_id", "flux", "flux_err", "filter", "bibcode"]]